WKCrashSDK - crash拦截工具
前言
由于线上始终出现部分未知原因崩溃问题,遂遵循网易出的crash拦截机制,自实现了一个crash拦截工具,现已上线运行数月,累计拦截闪退···总之很多啦···
5.png
实现原理
原理网上已有很多文章阐述,这里推荐几个链接。
网易iOS App运行时Crash自动防护实践
黑魔法教你让iOS APP防住Crash
优势:
- 封装完善,使用方便,仅需将文件导入项目即可生效。
- 具备debug期crash发生的UI层级提示。
- 可和线上接口配合实现实时开关操作。
- 可自定crashinfo上传地点(我司是直接上传到bugly搜集)
- 经过实际测试,已在我司多个线上APP实测有效,暂未发现有什么奇怪的问题。
项目要点
其实从上述原理文章以及能够了解基本的实现逻辑,只是在实现过程中也遇到了不少的坑。下面就和大家分享一下一些实现过程的坑以及为了满足我司需求拓展的一些功能点。
- KVO
1、拦截KVO时,存在部分三方库的不能拦截,以及系统的相机相册无需拦截,否则会出现无效的crash提示,在我的项目已经进行了白名单过滤。如果用了一些特殊的三方,可能在使用此工具时,需要收录一下,避免无效的才让身体是被收集。
//白名单主要针对观察者,因为被观察者很有可能是系统类,所以只能针对观察者处理,如果拦截到系统的观察者,则记录入白名单
+ (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文件需要使用mrl,在编译处添加-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层级的提示,如下图:
1.png
点击按钮可以查看具体的崩溃信息,如下图
2.png
前面title表示为崩溃的类型,后面数字为拦截的次数。
再次点击cell可定位崩溃的文件、对应方法名、最近一次崩溃发生的时间以及在本机上这个崩溃发生的次数。
3.png
4.png
大家可能也注意到了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一起交流进步。