问: 简述一下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冷启动之后依次判断
- App是否进行了升级
- 有没有通过
exit()
等函数退出App - App是否出现了crash
- 用户是否强制退出App
- 手机系统是否升级了
- App是否在后台,如果在后台就是发生了BOOM
- 上面所有条件都不满足,则是发生了FOOM
Xcode自带的 Allocations内存分析工具 可以检测App在开发过程中的内存占用情况,但是它的缺点也是这样的,因为只能检测到开发过程中,所以线上环境我们并不好检测到
常见的OOM检测工具有 OOMDetector, OOMDetector可以做OOM监控,以及内存泄漏检测等.
还有在开发过程中使用到的MLeaksFinder, MLeaksFinder主要使用到的原理是通过 当 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放.
问: 当出现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
,只能查看到如下引用图.
根本无法明显查看到循环引用问题~ 不盲目, 实践才是检验真理的唯一标准,请自行试验~
Comments | 0 条评论