前言
由于线上始终出现部分未知原因崩溃问题,遂遵循网易出的 crash 拦截机制,自实现了一个 crash 拦截工具,现已上线运行数月,累计拦截闪退···总之很多啦···
实现原理
原理网上已有很多文章阐述,这里推荐几个链接。
网易 iOS App 运行时 Crash 自动防护实践 [黑魔法教你让 iOS APP 防住 Crash](https://www.jianshu.com/p/021…
优势:
封装完善,使用方便,仅需将文件导入项目即可生效。
具备 debug 期 crash 发生的 UI 层级提示。
可和线上接口配合实现实时开关操作。
可自定 crashinfo 上传地点(我司是直接上传到 bugly 搜集)
经过实际测试,已在我司多个线上 APP 实测有效,暂未发现有什么奇怪的问题。
项目要点
其实从上述原理文章以及能够了解基本的实现逻辑,只是在实现过程中也遇到了不少的坑。下面就和大家分享一下一些实现过程的坑以及为了满足我司需求拓展的一些功能点。
KVO
这里划重点
1、拦截 KVO 时,存在部分三方库的不能拦截,以及系统的相机相册无需拦截,否则会出现无效的 crash 提示,在我的项目已经进行了白名单过滤。如果用了一些特殊的三方,可能在使用此工具时,需要收录一下,避免无效的 crashinfo 被收集。
// 白名单主要针对观察者,因为被观察者很有可能是系统类,所以只能针对观察者处理,如果拦截到系统的观察者,则记录入白名单
+ (NSArray *)kvoWhiteList
{
static NSArray *whiteList = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
whiteList = @[@”WKKVOProxy”,// 自己的
@”RACKVOProxy”,//RAC 的
@”BLYSDKManager”,//bugly 的
@”_YYTextKeyboardViewFrameObserver”,//YYKit 的
// 相册相关
@”PLManagedAlbum”,
@”AVCapturePhotoOutput”,
@”AVCaptureStillImageOutput”,
//3.2.9 添加 拍照相关
@”AVCaptureSession”,
@”PLPhotoStreamAlbum”,
@”AVKVODispatcher”,
@”PLCloudSharedAlbum”,
@”AVPlayerPropertyCache”,
];//@”AVCaptureFigVideoDevice”
});
return whiteList;
}
2、对 KVO 的拦截,需使用递归锁保证线程安全。
wk_pthread_mutex_init_recursive(&_lock,true);
pthread_mutex_lock(&_lock);
pthread_mutex_unlock(&_lock);
Zombie
划重点
在有僵尸对象造成崩溃时,实际是将其数据置为空,但是并不释放它,然后将其 isa 指向一个可接受任何方法的中转类中,以此来拦截掉崩溃。为了统一处理 crash 上报,在这里用了动态类创建传递类型信息的方式。并且.m 文件需要使用 MRC,在编译处添加 -fno-objc-arc 即可。
NSString *className = NSStringFromClass(selfClass);
NSString *zombieClassName = [@”WKZombie_” stringByAppendingString: className];// 这一步很重要,动态生成类,如果被僵尸,则可以得知实际是哪个类产生了僵尸指针 导致崩溃
Class zombieClass = NSClassFromString(zombieClassName);
if(!zombieClass) {
zombieClass = objc_allocateClassPair([WKZombieStub class], [zombieClassName UTF8String], 0);
}
objc_destructInstance(self);// 销毁实例 相关信息 内存不释放
object_setClass(self, zombieClass);
instanceList.size();
if (instanceList.size() >= maxCount) {
id object = instanceList.front();
instanceList.pop_front();
free(object);
}
instanceList.push_back(self);
Container
在拦截 NSArray 以及 NSDictionary 的系列方法时,需要注意一下它们的实现方式是类簇实现,需要找到它们真实的类来拦截才有效。
swizzling_exchangeMethod(objc_getClass(“__NSArray0”), @selector(objectAtIndex:), @selector(emptyArray_objectAtIndex:));
swizzling_exchangeMethod(objc_getClass(“__NSArrayI”), @selector(objectAtIndex:), @selector(arrayI_objectAtIndex:));
swizzling_exchangeMethod(objc_getClass(“__NSSingleObjectArrayI”), @selector(objectAtIndex:), @selector(singleObjectArrayI_objectAtIndex:));
划重点
在对 NSMutablArray 拦截时,需要特别注意其 objectAtIndex 的方法,需得在遵守 MRC 的文件下拦截,否则会在 iOS8 上弹出键盘时,APP 进入后台产生崩溃。是必现的。所以在工具中 这个方法是单独放到一个文件里面 hook 的,然后在编译处为此文件添加 -fno-objc-arc。
UI 层级提示信息
在 Debug 模式下,当拦截到 crash 时,会出现 UI 层级的提示,如下图:点击按钮可以查看具体的崩溃信息,如下图前面 title 表示为崩溃的类型,后面数字为拦截的次数。
再次点击 cell 可定位崩溃的文件、对应方法名、最近一次崩溃发生的时间以及在本机上这个崩溃发生的次数。
大家可能也注意到了 Crash 的按钮是可以随意拖动,以及根据你进入的大类型不同来变更提示信息的。一个可有可无的小优化~
CrashInfo 上报
CrashInfo 的收集,我们只需要关注 WKCrashReport 类,去实现它的一个代理即可。
@protocol WKCrashReportDelegate <NSObject>
– (void)handleCrashInfo:(WKCrashModel *)model type:(NSString *)type;
@end
返回的两个参数:WKCrashModel 以及 NSString type 其功用如下:
WKCrashModel
@interface WKCrashModel : NSObject
@property (nonatomic, strong) NSString * clasName; // 产生 crash 的类名
@property (nonatomic, strong) NSString * msg; //could be 方法名,或者其他有效信息
@property (nonatomic, strong) NSArray * threadStack;//crash 时的堆栈信息
@property (nonatomic, assign) NSTimeInterval time;//crash 时间
@property (nonatomic, strong, readonly) NSString * deviceType;// 设备信息
@property (nonatomic, strong, readonly) NSString * systemVersion;// 系统版本
@end
NSString type 其返回值可能有 UnrecognizedSelector,KVO,Container,Timer,NotificationCenter,Null,String,Zombie 分别代表八种拦截的 crash 类型
PS:如有特殊需求可自行扩充
使用方式
Demo 地址
进入 Demo 地址找到 WKCrashManagerDemo 里面的 WKCrashSDK 文件夹,拖入项目即可。后续我会抽空将其加入 cocoapods 豪华午餐~
注:如从 Demo 中直接拖入,则默认开启除了 Zomie 拦截外的其他 7 种类型的 crash 拦截。如需自定义请查看 WKCrashManager 的实现文件。
联系方式
如有兴趣可通过邮箱 357863248@qq.com 一起交流进步。