问: 简述一下iOS常见crash的原因.


  • 容器越界(字典, 数组, 字符串等) NSRangeException
  • 使用未初始化的变量 EXC_BAD_ACCESS
  • 用户授权问题
  • 选择器方法未定义 Unrecognized selector sent to instance
  • 子线程刷新UI
  • KVC 和 KVO 问题
  • 数据类型不匹配 (NSNumber, NSString, NSArray, NSDictionary, NSNull)
  • 内存溢出
  • 野指针
  • 死循环

问:如何在工作中预防容器越界?


除了我们的书写代码规范之外, 还可以利用 AOP编程 主要通过 Method Swizzling 来预防容器越界等问题.

先说一下前提条件,例如我们使用如下代码的过程中.

NSArray *array = @[@"1", @"2"];
NSLog(@"%@", array[10]);

程序就会发生崩溃,崩溃日志如下所示.

2023-04-17 03:32:31.281544+0800 17_Crash_Question[48076:1332657] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSConstantArray objectAtIndexedSubscript:]: index 10 beyond bounds [0 .. 1]'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff20405604 __exceptionPreprocess + 242
	1   libobjc.A.dylib                     0x00007fff201a4a45 objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff20486c63 _CFThrowFormattedException + 200
	3   CoreFoundation                      0x00007fff2033d8d7 +[NSConstantArray new] + 0
	4   17_Crash_Question                   0x000000010a9f9b3c -[ViewController touchesBegan:withEvent:] + 108
	5   UIKitCore                           0x00007fff250d4f2e forwardTouchMethod + 312
	6   UIKitCore                           0x00007fff250e5b33 -[UIWindow _sendTouchesForEvent:] + 617
	7   UIKitCore                           0x00007fff250e7e3a -[UIWindow sendEvent:] + 5312
	8   UIKitCore                           0x00007fff250bdeac -[UIApplication sendEvent:] + 820
	9   UIKitCore                           0x00007fff25155f0a __dispatchPreprocessedEventFromEventQueue + 5614
	10  UIKitCore                           0x00007fff2515935d __processEventQueue + 8635
	11  UIKitCore                           0x00007fff2514faf5 __eventFetcherSourceCallback + 232
	12  CoreFoundation                      0x00007fff203724a7 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
	13  CoreFoundation                      0x00007fff2037239f __CFRunLoopDoSource0 + 180
	14  CoreFoundation                      0x00007fff2037186c __CFRunLoopDoSources0 + 242
	15  CoreFoundation                      0x00007fff2036bf68 __CFRunLoopRun + 871
	16  CoreFoundation                      0x00007fff2036b704 CFRunLoopRunSpecific + 562
	17  GraphicsServices                    0x00007fff2cba9c8e GSEventRunModal + 139
	18  UIKitCore                           0x00007fff2509e65a -[UIApplication _run] + 928
	19  UIKitCore                           0x00007fff250a32b5 UIApplicationMain + 101
	20  17_Crash_Question                   0x000000010a9f9e7e main + 110
	21  dyld                                0x000000010ac19f21 start_sim + 10
	22  ???                                 0x000000010cb5351e 0x0 + 4508169502
)

这时候我们只需要使用 method swizzling 来交换方法, 就可以规避这种crash.

我们的思路是这样的,系统最好能主动调起我们写的method swizzling 方法, 这时候就有两种选择,一种是在 +load 方法中调用,另外一种是在 +initialize 方法中调用.一般情况下我们会选择在 +load 方法中调用,因为 +load 方法是在类加载的时候调用,而 +initialize 方法是在类第一次接收到消息的时候调用,如果NSArray接收消息的情况下(不 alloc), +initialize就不会调用.当然了,其实选择哪一种都可以的,只需要注意我们的时机即可. 所以我们会选择在 +load 方法中调用.

通过我们会写一个NSArray的Category分类, 分类名称就叫做 NSArray+Safe , 然后在 +load 方法中通过method swizzling 来交换方法实现.

+ (void)load {
	// 原始方法实现
    Method originalMethod = class_getInstanceMethod(self, @selector(objectAtIndexedSubscript:));
	// 安全方法实现
    Method swizzleMethod = class_getInstanceMethod(self, @selector(safe_objectAtIndexedSubscript:));
	// 交换方法实现
    method_exchangeImplementations(originalMethod, swizzleMethod);
}

- (id)safe_objectAtIndexedSubscript:(NSUInteger)idx {
    if (idx < self.count) {
        return [self safe_objectAtIndexedSubscript:idx];
    }
    NSLog(@"数组越界了 当前数组个数为 %ld", self.count);
    return nil;
}

如果我们使用如上的方法我们会发现,程序依然是崩溃的,这是为什么呢?其原因还是需要我们从crash日志中去寻找,我们发现crash崩溃日志中,消息的接受者实际上是 NSConstantArray.

NSConstantArray 是何物呢? 它实际上是 NSArray 的类簇.关于类簇相关内容,可以查看前一篇文章. 所以我们需要交换实际上是 NSConstantArray 中的方法实现.

借此再次改造一下我们的代码.

+ (void)load {
	// 原始方法实现
    Method originalMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndexedSubscript:));
	// 安全方法实现
    Method swizzleMethod = class_getInstanceMethod(self, @selector(safe_objectAtIndexedSubscript:));
	// 交换方法实现
    method_exchangeImplementations(originalMethod, swizzleMethod);
}

然后为了防止多次手动调用 +load 方法,我们一般需要添加一个dispath_once_t的标记.

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
		// 原始方法实现
        Method originalMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndexedSubscript:));
		// 安全方法实现
        Method swizzleMethod = class_getInstanceMethod(self, @selector(safe_objectAtIndexedSubscript:));
		// 交换方法实现
        method_exchangeImplementations(originalMethod, swizzleMethod);
    });
}

为了防止父类调用子类的情况,我们还需要加一层防护.


至此,常见的容器越界我们都是可以以这种形式进行处理, 但是还是要注意Method swizzling的使用,因为Method swizzling是一种黑科技,使用不当会导致一些不可预知的问题.


问:常见的crash收集工具有哪些? 你们是如何监控线上的crash的?


答:常见的crash收集工具有很多,比如说: Bugly,友盟,腾讯的Bugly,腾讯的CrashReport,百度的CrashReport,神策的CrashReport等等.

我先前公司使用的是腾讯的Bugly.


问:如何自己实现一个CrashReport? 或者说如何实现Crash的捕获?


想要实现一个CrashReport,我们需要做的事情就是在程序崩溃的时候,将崩溃的信息保存下来,然后在下次启动的时候将崩溃的信息上传到服务器.

整体的实现思路也是AOP编程.

首先想要是一个崩溃信息的收集,我们这里需要使用到的是 NSSetUncaughtExceptionHandler, 这是一个异常处理的回调函数,当程序发生异常崩溃时,我们通过这个 NSSetUncaughtExceptionHandler 来捕获异常.

我们查看了一下API,发现 NSSetUncaughtExceotionHandle 的参数为 NSUncaughtExceptionHandler类型的函数指针.

typedef void NSUncaughtExceptionHandler(NSException *exception);

FOUNDATION_EXPORT NSUncaughtExceptionHandler * _Nullable NSGetUncaughtExceptionHandler(void);
FOUNDATION_EXPORT void NSSetUncaughtExceptionHandler(NSUncaughtExceptionHandler * _Nullable);

我们写一个类实现一下这个回调函数.

这里需要注意的是,我们在保存的过程中为了防止程序崩溃,我们

void uncaughtExceptionHandler(NSException *exception) {
    NSArray * callStackSymbols = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];

    NSString* report = [NSString stringWithFormat:@"测试 \n%@\n%@\n%@", callStackSymbols, reason, name];

	// todo: 保存崩溃信息
    NSLog(@"%@", report);

	// todo: 保存崩溃信息
}

然后设置回调函数.

+ (void)exceptionInstall {
    NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
}

最后我们可以在 AppDelegate 中进行调用.

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
   [SDException exceptionInstall];
    return YES;
}

问: 什么叫做OOM?


OOM(out-of-memory)就是字面意思,是由iOS Jetsam 机制引起引起的异常crash,这种crash是不能被 NSSetUncaughtExceptionHandle 回调捕获.

OOM崩溃主要是分类BOOM(后台内存溢出Crash)和FOOM(前台内存溢出Crash).整体流程如下图所示.


问: 市面上有没有做过OOM治理? 如果做过,是如何做的?


关于内存警告系统提供了对应的函数方法,叫做didReciveMemoryWaring,但是这个函数并不一定会触发OOM. OOM也不一定会触发 didReciveMemoryWarning. 但是当我们收到内存警告之后,我们可以做一些释放工作,来避免一定程度上的OOM发生.

先前市面上对于OOM的治理主要是FaceBook团队提出的排除法, 如下所示.

在每次App冷启动之后依次判断

  1. App是否进行了升级
  2. 有没有通过 exit() 等函数退出App
  3. App是否出现了crash
  4. 用户是否强制退出App
  5. 手机系统是否升级了
  6. App是否在后台,如果在后台就是发生了BOOM
  7. 上面所有条件都不满足,则是发生了FOOM

Xcode自带的 Allocations内存分析工具 可以检测App在开发过程中的内存占用情况,但是它的缺点也是这样的,因为只能检测到开发过程中,所以线上环境我们并不好检测到

常见的OOM检测工具有 OOMDetector, OOMDetector可以做OOM监控,以及内存泄漏检测等.

还有在开发过程中使用到的MLeaksFinder, MLeaksFinder主要使用到的原理是通过 当 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放.

MLeaksFinder 原理


问: 当出现OOM之前一定会有 Memory Warning 吗?


不一定, 因为有可能一瞬间申请了大量内存,但是主线程又忙于其他事件, 这时候就可能导致还没来得及进行 Memory Warning, 程序就已经OOM了. 另外当出现 Memory Warning 也不是一定会出现 OOM,这里需要注意~


问: 你知道常见的循环引用问题是怎么产生的?


  • Object相互持有

    例如UIViewController中子视图持有self, 就会造成循环引用问题

  • Delegate的循环引用问题

    代理的循环引用问题主要是因为成员的修饰词不使用weak造成的

  • Block的循环引用问题

    block的循环引用问题也是相互引用问题造成的.

  • NSTimer的引用问题

    NSTimer的造成的内存泄露并不是循环引用造成的,而是 RunLoop 持有了 NSTimer, NSTimer持有了对象本身造成的,所以不管对象本身是否持有NSTimer都会有一定的内存泄露隐患.


问: 平常是如何 Debug Memory Graph 检测内存泄露问题?


Debug Memory Graph 可以有效的查看引用关系,这样我们可以配合着 MLeaksFinder 使用,利用 MLeaksFinder 检测到泄露之后,然后用 Debug Memory Graph 查看具体的引用关系.

这里我举一个使用实例.

首先我们先构造一个循环引用的示例代码.

NSMutableArray *array1 = [[NSMutableArray array] init];
NSMutableArray *array2 = [[NSMutableArray array] init];

[array1 addObject:array2];
[array2 addObject:array1];

然后我们按图配置一下运行选项.

然后,我们运行一下项目, 首先手动执行一下如上代码.

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    NSMutableArray *array1 = [[NSMutableArray array] init];
    NSMutableArray *array2 = [[NSMutableArray array] init];

    [array1 addObject:array2];
    [array2 addObject:array1];
}

按图点击如下按钮进入 Debug Memory Graph

然后我们可以通过Show only leaks block筛选按钮,让列表只展示有内存泄露的部分.

这时候我们发现有内存泄露问题,如图所示.

我们点击进去就可以在右边查看到如下的引用图关系, 我们可以通过此图就可以很清楚的了解到循环引用关系.

但是,我发现虽然这种循环引用是可以查看到的,但是当我们使用两个类相互引用就不会展示(如下所示),

#import "TestViewController.h"
#import "Object.h"

@interface TestViewController ()

@property(nonatomic, strong)Object *obj;

@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor orangeColor];
    self.obj = [[Object alloc] init];
    self.obj.vc = self;
}
#import <UIKit/UIKit.h>

@interface Object : NSObject

@property(nonatomic,strong)UIViewController *vc;

@end

然后这时候我们再次查看 Debug Memory Graph ,只能查看到如下引用图.

根本无法明显查看到循环引用问题~ 不盲目, 实践才是检验真理的唯一标准,请自行试验~


IT界无底坑洞栋主 欢迎加Q骚扰:676758285