前言
上周看了一篇掘友的文章——APM – iOS Crash监控 KSCrash代码解析,主要就是对KSCrash这个框架的源码做了分析。
最近手上正好有个项目要集成崩溃跟踪相关功能,仔细看了一下掘友的这篇文章,顺带也在Github上面了解一下这个项目。于是决定用KSCrash在项目中。
我决定使用KSCrash有以下2个原因:
- 一开始我是推荐公司使用Bugly的,但是项目负责人意思是不希望崩溃相关的数据在其他平台上,还是希望自己能管控起来。
- 希望能够找到开源、高质量的bug跟踪工具,说白了就是希望集成成本低,功能又不错。
于是乎,就有了这篇文章。
KSCrash的集成
KSCrash支持cocopods,所以需要像官方文档中写的那样手动集成,直接在Podfile中添加后,一句pod install
就搞定了。
另外文档中说明了KSCrash说明了上传崩溃日志到后台的方式,我简单说一下。
KSCrash can report to the following servers:
- Hockey:网页打不开了,感觉应该用不了。
- QuincyKit:这个应该是要自己搭建一个php服务器,同时还需要在App中集成另外的SDK配合使用,好像没有必要,我只需要将崩溃日志上传到项目的服务器即可。
- Victory:An error reporting server in Python. It runs on Google App Engine.一个用Python写的崩溃跟踪管理平台,也没有必要。
- Email:这个比较适合个人开发者,有崩溃后,通过邮件的形式发给开发者的邮箱中。
- Standard:上传到自定义的URL中,进行网络请求并上传。
怎么看都觉得Standard
比较适合我需要的方式,代码立马走起,因为我的项目主要是使用Swift,所以这里都通过Swift代码进行展示:
func installCrashHandler() {
let installation = makeStandardInstallation()
installation.sendAllReports { array, completed, error in
if completed {
print("Sent \(array?.count ?? 0) reports")
} else {
print("Failed to send reports: \(error.debugDescription)")
}
}
installation.install()
}
private func makeStandardInstallation() -> KSCrashInstallation {
let standard = KSCrashInstallationStandard.sharedInstance()!
let url = URL(string: "崩溃日志上传地址")
standard.url = url
return standard
}
最后我们只需要将installCrashHandler()
这个方法在func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
中调用即可。
为了看看崩溃日志的收集效果,我特地在自己项目里面写了一个数组越界,然后去沙盒捞了一把日志,我只摘要其中的关键信息:
"threads": [
{
"backtrace": {
"contents": [
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "<redacted>",
"symbol_addr": 7358277980,
"instruction_addr": 7358278340
},
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "<redacted>",
"symbol_addr": 7358277980,
"instruction_addr": 7358278340
},
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "<redacted>",
"symbol_addr": 7358277476,
"instruction_addr": 7358277672
},
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "<redacted>",
"symbol_addr": 7358276960,
"instruction_addr": 7358277168
},
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_A2HSus6UInt32VtF",
"symbol_addr": 7358275744,
"instruction_addr": 7358275976
},
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "$ss12_ArrayBufferV37_checkInoutAndNativeTypeCheckedBounds_03wasfgH0ySi_SbtF",
"symbol_addr": 7358125164,
"instruction_addr": 7358125444
},
{
"object_name": "libswiftCore.dylib",
"object_addr": 7358046208,
"symbol_name": "$sSayxSicig",
"symbol_addr": 7358144088,
"instruction_addr": 7358144176
},
{
"object_name": "RxStudy",
"object_addr": 4308205568,
"symbol_name": "$s7RxStudy16HotKeyControllerC7setupUI33_D55B00154F4922155B5D3E54789A2010LLyyF",
"symbol_addr": 4309867052,
"instruction_addr": 4309869376
},
{
"object_name": "RxStudy",
"object_addr": 4308205568,
"symbol_name": "$s7RxStudy16HotKeyControllerC11viewDidLoadyyF",
"symbol_addr": 4309866844,
"instruction_addr": 4309866936
},
{
"object_name": "RxStudy",
"object_addr": 4308205568,
"symbol_name": "$s7RxStudy16HotKeyControllerC11viewDidLoadyyFTo",
"symbol_addr": 4309866992,
"instruction_addr": 4309867028
}
.
.
.
.
.
.
我大概整理一下这段json中的一些关键信息:
- $ss12_ArrayBufferV37_checkInoutAndNativeTypeCheckedBounds_03wasfgH0ySi_SbtF
- $s7RxStudy16HotKeyControllerC7setupUI33_D55B00154F4922155B5D3E54789A2010LLyyF
- “$s7RxStudy16HotKeyControllerC11viewDidLoadyyF”
在项目RxStudy中的HotKeyController的viewDidLoad方法中的setupUI方法中,尝试checkInoutAndNativeTypeCheckedBounds
而崩溃了。
利用搜索或者AI你可以很容易的查到checkInoutAndNativeTypeCheckedBounds
方法是和数组越界有关的崩溃,到此,我觉得KSCrash的Bug跟踪基本符合我的预期。
可以定位到具体页面,而不用去自己符号化,另外也基本上告诉了崩溃的原因。
当然,我这里只是一个故意的崩溃,更多的考验只能通过真实的项目去考验。
如何上传崩溃日志
大家如果仔细看,可以看到上面的代码中有这样一个方法installation.sendAllReports
,这个方法会在有崩溃的情况下,自动进行日志的上传,我们可以追着这个函数,找到其具体实现:
- (void) filterReports:(NSArray*) reports
onCompletion:(KSCrashReportFilterCompletion) onCompletion
{
NSError* error = nil;
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:15];
KSHTTPMultipartPostBody* body = [KSHTTPMultipartPostBody body];
NSData* jsonData = [KSJSONCodec encode:reports
options:KSJSONEncodeOptionSorted
error:&error];
if(jsonData == nil)
{
kscrash_callCompletion(onCompletion, reports, NO, error);
return;
}
[body appendData:jsonData
name:@"reports"
contentType:@"application/json"
filename:@"reports.json"];
// TODO: Disabled gzip compression until support is added server side,
// and I've fixed a bug in appendUTF8String.
// [body appendUTF8String:@"json"
// name:@"encoding"
// contentType:@"string"
// filename:nil];
request.HTTPMethod = @"POST";
request.HTTPBody = [body data];
[request setValue:body.contentType forHTTPHeaderField:@"Content-Type"];
[request setValue:@"KSCrashReporter" forHTTPHeaderField:@"User-Agent"];
// [request setHTTPBody:[[body data] gzippedWithError:nil]];
// [request setValue:@"gzip" forHTTPHeaderField:@"Content-Encoding"];
self.reachableOperation = [KSReachableOperationKSCrash operationWithHost:[self.url host] allowWWAN:YES
block:^
{
[[KSHTTPRequestSender sender] sendRequest:request
onSuccess:^(__unused NSHTTPURLResponse* response, __unused NSData* data)
{
kscrash_callCompletion(onCompletion, reports, YES, nil);
} onFailure:^(NSHTTPURLResponse* response, NSData* data)
{
NSString* text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
kscrash_callCompletion(onCompletion, reports, NO,
[NSError errorWithDomain:[[self class] description]
code:response.statusCode
userInfo:[NSDictionary dictionaryWithObject:text
forKey:NSLocalizedDescriptionKey]
]);
} onError:^(NSError* error2)
{
kscrash_callCompletion(onCompletion, reports, NO, error2);
}];
}];
}
这是一段OC代码,虽然看着有点长,但是其实本质上就是通过原生的方法进行网络请求,然后就上传了。
不过这方法有个非常非常严重的问题,那就是无法对网络请求的请求头和请求参数进行自主配置!
比如我想上传崩溃日志的时候,带上cid,抱歉,办不到!
甚至,是我的项目中,规定需要在请求头带特定的参数才行,无法自定义配置,就连网络请求都会被拒绝。
看来通过对KSCrashInstallationStandard
单例来配置url进行网络请求,并不是特别好的方案。
绕了一圈,崩溃日志可以抓取到,但是无法通过现有的API进行上传,该怎么进行优化呢?
此路不通,我们换条路走走
如果我能拿到KSCrash的崩溃数据,自己按照自己的业务线逻辑走网络请求不就好了?
于是我就去翻了翻KSCrash的API,其实非常简单,在KSCrash.h
就有啦:
/** Get all unsent report IDs.
*
* @return An array with report IDs.
*/
- (NSArray*) reportIDs;
/** Get report.
*
* @param reportID An ID of report.
*
* @return A dictionary with report fields. See KSCrashReportFields.h for available fields.
*/
- (NSDictionary*) reportWithID:(NSNumber*) reportID;
获取崩溃日志的reportIDs,通过reportID拿到一个NSDictionary的崩溃报告。
考虑到我使用Moya进行上传操作,操作就简单了。
我写了一个KCrash分类,直接拿到所有崩溃日志的数据:
import KSCrash
extension KSCrash {
func getCrashData() -> Data? {
let array = KSCrash.sharedInstance().reportIDs()
if let ids = array as? [NSNumber],
ids.isNotEmpty {
let jsons = ids.map {
var json = KSCrash.sharedInstance().report(withID: $0)
json?["binary_images"] = nil
return json
}
return try? KSJSONCodec.encode(jsons, options: KSJSONEncodeOption(rawValue: 2))
} else {
return nil
}
}
}
为了简洁,我全部都是用的KSCrash里面的转换方法,只是通过Swift语言,通过高阶函数,一步把[reportID]
转成[Dictionary]
最后转成Data?
。
这样一来,当Data?
不为nil的时候,我就通过自己的网络请求去上传崩溃日志即可。
同时网络请求成功后,我就上传成功的日志清理掉就可以了,调用方法KSCrash.sharedInstance().deleteAllReports()
即可。
总结
这篇文章,是对KSCrash在自己项目中的一点使用心得,从集成、验证、查阅源码和简单改造。
在阅读KSCrash源码的过程中,我真的觉得这代码写的不简单,C、C++、OC都用上了。虽然年代久远了,不过还是有一些借鉴学习意义。同时我表示看不懂???
参考文档
自己写的项目,欢迎大家star⭐️
RxStudy:RxSwift/RxCocoa框架,MVVM模式编写wanandroid客户端。
GetXStudy:使用GetX,重构了Flutter wanandroid客户端。