郑培林 發表於 2024-7-18 17:24:00

iOS开发基础133-崩溃预防

<p>现代移动应用的用户体验依赖于其稳定性和可靠性。然而,在开发过程中,我们时常会遇到各种崩溃问题。崩溃不仅会影响用户的使用体验,还可能损害应用的声誉。因此,本文将详细介绍一个名为CrashPrevention的工具类,它能够为iOS开发者提供多方面的崩溃预防措施,借助该工具类,开发者能够有效减少崩溃的发生,并提升应用的稳定性。</p>
<h3 id="crashprevention工具类概述">CrashPrevention工具类概述</h3>
<p>CrashPrevention是一个易于集成的工具类,专为iOS应用中的多种常见崩溃情况提供预防措施。通过调用相关方法,开发者可以开启针对数组操作、字典操作、未识别的选择器、通知中心、键值观察(KVO)、字符串操作、多线程操作以及UI线程操作的保护机制。特别值得一提的是,CrashPrevention让开发者可以通过一个全局的 <code>isDebug</code> 标志,灵活控制是否启用这些崩溃预防措施。</p>
<h3 id="crashpreventionh-头文件">CrashPrevention.h 头文件</h3>
<p>首先,我们来看一下CrashPrevention的头文件,其中定义了所有的预防方法:</p>
<pre><code class="language-objc">#import &lt;Foundation/Foundation.h&gt;
#import &lt;UIKit/UIKit.h&gt;

@interface CrashPrevention : NSObject

// 设置是否在 Debug 模式中启用崩溃预防
+ (void)setDebugMode:(BOOL)isDebug;

// 启用所有崩溃保护
+ (void)enableAllCrashPrevention;

// 启用各类崩溃保护
+ (void)enableArrayProtection;
+ (void)enableDictionaryProtection;
+ (void)enableSelectorProtection;
+ (void)enableNotificationProtection;
+ (void)enableKVOCrashProtection;
+ (void)enableStringProtection;
+ (void)enableThreadSafetyProtection;
+ (void)enableUIThreadProtection;

@end
</code></pre>
<h3 id="crashpreventionm-实现文件">CrashPrevention.m 实现文件</h3>
<p>接下来,我们深入了解实现文件的设计思路和具体代码。</p>
<h4 id="全局debug标志">全局Debug标志</h4>
<pre><code class="language-objc">@implementation CrashPrevention

// 用于记录是否在 Debug 模式下启用崩溃预防
static BOOL debugModeEnabled = NO;

// 设置是否在 Debug 模式中启用崩溃预防
+ (void)setDebugMode:(BOOL)isDebug {
    debugModeEnabled = isDebug;
}
</code></pre>
<p>通过 <code>static BOOL debugModeEnabled</code>,我们可以记录是否启用调试模式,基于此标志决定是否启用崩溃预防功能。</p>
<h4 id="启用所有崩溃保护">启用所有崩溃保护</h4>
<pre><code class="language-objc">+ (void)enableAllCrashPrevention {
    if (!debugModeEnabled) {
      return;
    }
    ;
    ;
    ;
    ;
    ;
    ;
    ;
    ;
}
</code></pre>
<p>该方法通过检查 <code>debugModeEnabled</code> 标志,决定是否依次启用各类崩溃保护。</p>
<h4 id="数组越界保护">数组越界保护</h4>
<pre><code class="language-objc">+ (void)enableArrayProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayI")
                           original:@selector(objectAtIndex:)
                           swizzled:@selector(safe_objectAtIndex:)];
      [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayM")
                           original:@selector(objectAtIndex:)
                           swizzled:@selector(safe_objectAtIndex:)];
    });
}

- (id)safe_objectAtIndex:(NSUInteger)index {
    if (index &lt; self.count) {
      return ;
    } else {
      @try {
            NSLog(@"Array index out of bound: %lu", (unsigned long)index);
      } @catch (NSException *exception) {
            // 处理异常
      }
      return nil;
    }
}
</code></pre>
<p>通过 <code>Method Swizzling</code>,我们可以将数组的 <code>objectAtIndex:</code> 方法替换为安全版本。在越界的情况下,返回 <code>nil</code> 并记录日志,而不会崩溃。</p>
<h4 id="字典键值检查保护">字典键值检查保护</h4>
<pre><code class="language-objc">+ (void)enableDictionaryProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      [self swizzleInstanceMethod:NSClassFromString(@"__NSDictionaryI")
                           original:@selector(objectForKey:)
                           swizzled:@selector(safe_objectForKey:)];
    });
}

- (id)safe_objectForKey:(id)key {
    if (key) {
      return ;
    } else {
      @try {
            NSLog(@"Attempted to access dictionary with nil key");
      } @catch (NSException *exception) {
            // 处理异常
      }
      return nil;
    }
}
</code></pre>
<p>类似地,通过 <code>Method Swizzling</code>,我们可以将 <code>objectForKey:</code> 方法替换为安全版本,防止使用 <code>nil</code> 作为键值时的崩溃。</p>
<h4 id="消息转发保护">消息转发保护</h4>
<pre><code class="language-objc">+ (void)enableSelectorProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      Method forwardMethod = class_getInstanceMethod(, @selector(forwardingMethod:));
      class_addMethod(, NSSelectorFromString(@"unrecognizedSelectorHandler"), method_getImplementation(forwardMethod), method_getTypeEncoding(forwardMethod));
    });
}

- (void)forwardingMethod:(SEL)aSelector {}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (!debugModeEnabled) {
      return ;
    }
    class_addMethod(, sel, class_getMethodImplementation(, @selector(forwardingMethod:)), "v@:");
    return YES;
}
</code></pre>
<p>通过添加一个默认的 <code>forwardingMethod: </code>,我们防止调用未实现的方法时崩溃。</p>
<h4 id="通知中心保护">通知中心保护</h4>
<pre><code class="language-objc">+ (void)enableNotificationProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      
                           original:@selector(addObserver:selector:name:object:)
                           swizzled:@selector(safe_addObserver:selector:name:object:)];
      
                           original:@selector(removeObserver:name:object:)
                           swizzled:@selector(safe_removeObserver:name:object:)];
    });
}

- (void)safe_addObserver:(NSObject *)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject {
    if (observer) {
      [ safe_addObserver:observer selector:aSelector name:aName object:anObject];
    } else {
      @try {
            NSLog(@"Attempted to add a nil observer for name: %@", aName);
      } @catch (NSException *exception) {
            // 处理异常
      }
    }
}

- (void)safe_removeObserver:(NSObject *)observer name:(NSString *)aName object:(id)anObject {
    if (observer) {
      [ safe_removeObserver:observer name:aName object:anObject];
    } else {
      @try {
            NSLog(@"Attempted to remove a nil observer for name: %@", aName);
      } @catch (NSException *exception) {
            // 处理异常
      }
    }
}
</code></pre>
<p>在添加和移除观察者时进行空检查,防止空观察者导致的崩溃。</p>
<h4 id="kvo-保护">KVO 保护</h4>
<pre><code class="language-objc">+ (void)enableKVOCrashProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      
                           original:@selector(addObserver:forKeyPath:options:context:)
                           swizzled:@selector(safe_addObserver:forKeyPath:options:context:)];
      
                           original:@selector(removeObserver:forKeyPath:)
                           swizzled:@selector(safe_removeObserver:forKeyPath:)];
    });
}

- (void)safe_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    if (observer &amp;&amp; keyPath) {
      ;
    } else {
      @try {
            NSLog(@"Attempted to add observer with nil observer or key path: %@", keyPath);
      } @catch (NSException *exception) {
            // 处理异常
      }
    }
}

- (void)safe_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    if (observer &amp;&amp; keyPath) {
      ;
    } else {
      @try {
            NSLog(@"Attempted to remove observer with nil observer or key path: %@", keyPath);
      } @catch (NSException *exception) {
            // 处理异常
      }
    }
}
</code></pre>
<p>在添加和移除KVO时进行必要检查,确保参数合法,防止崩溃。</p>
<h4 id="字符串越界检查">字符串越界检查</h4>
<pre><code class="language-objc">+ (void)enableStringProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString")
                           original:@selector(substringFromIndex:)
                           swizzled:@selector(safe_substringFromIndex:)];
    });
}

- (NSString *)safe_substringFromIndex:(NSUInteger)from {
    if (from &lt;= self.length) {
      return ;
    } else {
      @try {
            NSLog(@"String index out of bound: %lu", (unsigned long)from);
      } @catch (NSException *exception) {
            // 处理异常
      }
      return nil;
    }
}
</code></pre>
<p>通过交换NSString的相关方法,确保在越界访问时返回 <code>nil</code> 并记录日志,从而避免崩溃。</p>
<h4 id="线程安全保护">线程安全保护</h4>
<pre><code class="language-objc">+ (void)enableThreadSafetyProtection {
    if (!debugModeEnabled) {
      return;
    }
    // 实现是与具体使用场景相关的,需要结合项目实际情况实现
}
</code></pre>
<p>这部分的实现高度依赖于具体的使用场景,比如可以使用 <code>dispatch_barrier_async</code>、<code>NSLock</code> 等技术实现。</p>
<h4 id="ui线程保护">UI线程保护</h4>
<pre><code class="language-objc">+ (void)enableUIThreadProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      
                           original:@selector(setNeedsLayout)
                           swizzled:@selector(safe_setNeedsLayout)];
    });
}

- (void)safe_setNeedsLayout {
    if () {
      ;
    } else {
      dispatch_async(dispatch_get_main_queue(), ^{
            ;
      });
      @try {
            NSLog(@"setNeedsLayout was called off the main thread. Fixed by dispatching to main queue.");
      } @catch (NSException *exception) {
            // 处理异常
      }
    }
}
</code></pre>
<p>确保UI操作总在主线程进行,如果不是,则调度到主线程执行,并记录警告日志。</p>
<h4 id="方法交换">方法交换</h4>
<pre><code class="language-objc">#pragma mark - Method Swizzling
+ (void)swizzleInstanceMethod:(Class)cls original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originalMethod = class_getInstanceMethod(cls, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);

    BOOL didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
      class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
      method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}
@end
</code></pre>
<p>核心方法交换逻辑,通过 <code>Method Swizzling</code> 替换原有的方法实现。</p>
<h3 id="使用crashprevention工具类">使用CrashPrevention工具类</h3>
<p>在应用启动时初始化CrashPrevention,并设置是否启用调试模式:</p>
<pre><code class="language-objc">- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 设置 Debug 模式
    #ifdef DEBUG
      ;
    #else
      ;
    #endif
   
    ;
    return YES;
}
</code></pre>
<h3 id="总结">总结</h3>
<p>CrashPrevention工具类为iOS开发者提供了多个方面的崩溃预防措施,通过简单调用,即可为数组、字典、未识别选择器、通知中心、KVO、字符串、多线程和UI线程的操作提供全面的保护。特别是通过 <code>isDebug</code> 标志,让开发者可以灵活控制这些预防措施在调试阶段和正式发布中的启用状态。借助这一工具类,开发者能够有效减少崩溃问题的发生,提升应用的稳定性和用户体验。</p>
<p>最后附上完整代码:</p>
<h4 id="crashpreventionh文件">CrashPrevention.h文件</h4>
<pre><code class="language-objc">#import &lt;Foundation/Foundation.h&gt;
#import &lt;UIKit/UIKit.h&gt;

@interface CrashPrevention : NSObject

// 设置是否在 Debug 模式中启用崩溃预防
+ (void)setDebugMode:(BOOL)isDebug;

// 启用所有崩溃保护
+ (void)enableAllCrashPrevention;

// 启用各类崩溃保护
+ (void)enableArrayProtection;
+ (void)enableDictionaryProtection;
+ (void)enableSelectorProtection;
+ (void)enableNotificationProtection;
+ (void)enableKVOCrashProtection;
+ (void)enableStringProtection;
+ (void)enableThreadSafetyProtection;
+ (void)enableUIThreadProtection;

@end

</code></pre>
<h4 id="crashpreventionm文件">CrashPrevention.m文件</h4>
<pre><code class="language-objc">#import "CrashPrevention.h"
#import &lt;objc/runtime.h&gt;

@implementation CrashPrevention

// 用于记录是否在 Debug 模式下启用崩溃预防
static BOOL debugModeEnabled = NO;

// 设置是否在 Debug 模式中启用崩溃预防
+ (void)setDebugMode:(BOOL)isDebug {
    debugModeEnabled = isDebug;
}

// 启用所有崩溃保护
+ (void)enableAllCrashPrevention {
    if (!debugModeEnabled) {
      return;
    }
    ;
    ;
    ;
    ;
    ;
    ;
    ;
    ;
}

#pragma mark - Array Protection
+ (void)enableArrayProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayI")
                           original:@selector(objectAtIndex:)
                           swizzled:@selector(safe_objectAtIndex:)];
      [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayM")
                           original:@selector(objectAtIndex:)
                           swizzled:@selector(safe_objectAtIndex:)];
    });
}

- (id)safe_objectAtIndex:(NSUInteger)index {
    if (index &lt; self.count) {
      return ;
    } else {
      @try {
            NSLog(@"Array index out of bound: %lu", (unsigned long)index);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            return nil;
      }
    }
}

#pragma mark - Dictionary Protection
+ (void)enableDictionaryProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      [self swizzleInstanceMethod:NSClassFromString(@"__NSDictionaryI")
                           original:@selector(objectForKey:)
                           swizzled:@selector(safe_objectForKey:)];
      [self swizzleInstanceMethod:NSClassFromString(@"__NSDictionaryM")
                           original:@selector(objectForKey:)
                           swizzled:@selector(safe_objectForKey:)];
    });
}

- (id)safe_objectForKey:(id)key {
    if (key) {
      return ;
    } else {
      @try {
            NSLog(@"Attempted to access dictionary with nil key");
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            return nil;
      }
    }
}

#pragma mark - Selector Protection
+ (void)enableSelectorProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      Method forwardMethod = class_getInstanceMethod(, @selector(forwardingMethod:));
      class_addMethod(, NSSelectorFromString(@"unrecognizedSelectorHandler"), method_getImplementation(forwardMethod), method_getTypeEncoding(forwardMethod));
    });
}

- (void)forwardingMethod:(SEL)aSelector {}

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (!debugModeEnabled) {
      return ;
    }
    class_addMethod(, sel, class_getMethodImplementation(, @selector(forwardingMethod:)), "v@:");
    return YES;
}

#pragma mark - Notification Protection
+ (void)enableNotificationProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      
                           original:@selector(addObserver:selector:name:object:)
                           swizzled:@selector(safe_addObserver:selector:name:object:)];
      
                           original:@selector(removeObserver:name:object:)
                           swizzled:@selector(safe_removeObserver:name:object:)];
      
                           original:@selector(removeObserver:)
                           swizzled:@selector(safe_removeObserver:)];
    });
}

- (void)safe_addObserver:(NSObject *)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject {
    if (observer) {
      [ safe_addObserver:observer selector:aSelector name:aName object:anObject];
    } else {
      @try {
            NSLog(@"Attempted to add a nil observer for name: %@", aName);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

- (void)safe_removeObserver:(NSObject *)observer {
    if (observer) {
      [ safe_removeObserver:observer];
    } else {
      @try {
            NSLog(@"Attempted to remove a nil observer");
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

- (void)safe_removeObserver:(NSObject *)observer name:(NSString *)aName object:(id)anObject {
    if (observer) {
      [ safe_removeObserver:observer name:aName object:anObject];
    } else {
      @try {
            NSLog(@"Attempted to remove a nil observer for name: %@", aName);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

#pragma mark - KVO Protection
+ (void)enableKVOCrashProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      
                           original:@selector(addObserver:forKeyPath:options:context:)
                           swizzled:@selector(safe_addObserver:forKeyPath:options:context:)];
      
                           original:@selector(removeObserver:forKeyPath:)
                           swizzled:@selector(safe_removeObserver:forKeyPath:)];
    });
}

- (void)safe_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
    if (observer &amp;&amp; keyPath) {
      ;
    } else {
      @try {
            NSLog(@"Attempted to add observer with nil observer or key path: %@", keyPath);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

- (void)safe_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
    if (observer &amp;&amp; keyPath) {
      ;
    } else {
      @try {
            NSLog(@"Attempted to remove observer with nil observer or key path: %@", keyPath);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

#pragma mark - String Protection
+ (void)enableStringProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString")
                           original:@selector(substringFromIndex:)
                           swizzled:@selector(safe_substringFromIndex:)];
      [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString")
                           original:@selector(substringToIndex:)
                           swizzled:@selector(safe_substringToIndex:)];
      [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString")
                           original:@selector(substringWithRange:)
                           swizzled:@selector(safe_substringWithRange:)];
    });
}

- (NSString *)safe_substringFromIndex:(NSUInteger)from {
    if (from &lt;= self.length) {
      return ;
    } else {
      @try {
            NSLog(@"String index out of bound: %lu", (unsigned long)from);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            return nil;
      }
    }
}

- (NSString *)safe_substringToIndex:(NSUInteger)to {
    if (to &lt;= self.length) {
      return ;
    } else {
      @try {
            NSLog(@"String index out of bound: %lu", (unsigned long)to);
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            return nil;
      }
    }
}

- (NSString *)safe_substringWithRange:(NSRange)range {
    if (range.location + range.length &lt;= self.length) {
      return ;
    } else {
      @try {
            NSLog(@"String range out of bound: %@", NSStringFromRange(range));
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            return nil;
      }
    }
}

#pragma mark - Thread Safety Protection
+ (void)enableThreadSafetyProtection {
    if (!debugModeEnabled) {
      return;
    }
    // 实现是与具体使用场景相关的,需要结合项目实际情况实现
}

#pragma mark - UI Thread Protection
+ (void)enableUIThreadProtection {
    if (!debugModeEnabled) {
      return;
    }
    static dispatch_once_t onceToken;
    dispatch_once(&amp;onceToken, ^{
      
                           original:@selector(setNeedsLayout)
                           swizzled:@selector(safe_setNeedsLayout)];
      
                           original:@selector(setNeedsDisplay)
                           swizzled:@selector(safe_setNeedsDisplay)];
      
                           original:@selector(setNeedsDisplayInRect:)
                           swizzled:@selector(safe_setNeedsDisplayInRect:)];
    });
}

- (void)safe_setNeedsLayout {
    if () {
      ;
    } else {
      dispatch_async(dispatch_get_main_queue(), ^{
            ;
      });
      @try {
            NSLog(@"setNeedsLayout was called off the main thread. Fixed by dispatching to main queue.");
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

- (void)safe_setNeedsDisplay {
    if () {
      ;
    } else {
      dispatch_async(dispatch_get_main_queue(), ^{
            ;
      });
      @try {
            NSLog(@"setNeedsDisplay was called off the main thread. Fixed by dispatching to main queue.");
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

- (void)safe_setNeedsDisplayInRect:(CGRect)rect {
    if () {
      ;
    } else {
      dispatch_async(dispatch_get_main_queue(), ^{
            ;
      });
      @try {
            NSLog(@"setNeedsDisplayInRect: was called off the main thread. Fixed by dispatching to main queue.");
      } @catch (NSException *exception) {
            // 处理异常
      } @finally {
            // 什么也不做
      }
    }
}

#pragma mark - Method Swizzling
+ (void)swizzleInstanceMethod:(Class)cls original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originalMethod = class_getInstanceMethod(cls, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);

    BOOL didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
    if (didAddMethod) {
      class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
      method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end

</code></pre>


</div>
<div id="MySignature" role="contentinfo">
    将来的你会感谢今天如此努力的你!
版权声明:本文为博主原创文章,未经博主允许不得转载。<br><br>
来源:https://www.cnblogs.com/chglog/p/18310066
頁: [1]
查看完整版本: iOS开发基础133-崩溃预防