異常崩潰怎么辦?
關于異常崩潰是每個App都要面對的,平時開發(fā)還好,在調(diào)試狀態(tài)下遇到的問題,可以通過LogCat打印的異常日志信息進行分析處理,但是一旦App上線后,大量用戶安裝了你的應用,每個用戶的手機大小、傳感器、SDK版本都不盡相同,可能你在測試機上跑的穩(wěn)穩(wěn)的應用,到了客戶手機上就會出現(xiàn)一些莫名其妙的異常,如果只是一些內(nèi)存泄露的問題可能還好,最起碼不會瞬間崩潰,但是如果遇到一些可以導致手機崩潰Bug的話,你讓出問題的用戶來復現(xiàn)Bug是不可能的,所以,全局異常捕獲就顯得很重要了,而DhccCrashLib就是一個全局異常捕獲的組件。
DhccCrashLib怎么用?
使用方法還是比較簡單的,首先在項目的根目錄下的build.gradle中加入Jcenter倉庫:
repositories {
jcenter()
}
然后在你的項目的build.gradle中添加依賴:
implementation 'com.dhcc.crashlib:CrashLib:1.0.3'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.github.zhaokaiqiang.klog:library:1.6.0'
implementation "com.sun.mail:android-mail:1.6.0"
這四個依賴都需要加,因為擔心版本沖突,所以我在組件中使用的依賴方式是compileOnly,那么你在你的項目中如果有引用除了CrashLib外的這三個依賴的話,就可以換成你自己的版本號即可。
使用方式 在你項目的自定義Application中:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
initCrash();
}
/**
* 初始化崩潰采集服務
*/
private void initCrash() {
EmailConfigBean emailConfigBean = new EmailConfigBean("你的發(fā)送郵箱", "你的接收郵箱", "你的發(fā)送郵箱密碼");
Configuration configuration=Configuration.getInstance()
//你的郵件配置實例
.setEmailConfig(emailConfigBean)
//是否通過郵件發(fā)送異常
.setSendWithEmail(true)
//是否通過郵件發(fā)送異常并將本地存儲的異常已附件的形式發(fā)送
.setSendEmailWithFile(true)
//異常服務器的API
.setCrashServerUrl("http://111.222.333.444:9999/api/crashs")
//是否給服務器發(fā)送異常信息
.setSendWithNet(true)
//異常的描述信息
.setCrashDescription("測試異常~~")
//捕獲異常后退出App的等待時間 毫秒
.setExitWaitTime(5000)
;
LogCenter.getLogCenter("進程名", configuration)
//可以自定義異常 只要實現(xiàn)ICollector 并傳入網(wǎng)絡提交時所需要的key即可
.strategy(new TestCollectInfo(), "網(wǎng)絡屬性的Key")
.init(this);
}
}
就這么簡單,首先先把你的發(fā)送郵箱和接收郵箱的相關信息都配置到EmailConfigBean中去,然后再調(diào)用LogCenter初始化相關參數(shù)即可,不過這里有一個細節(jié)需要講一下,注意看TestCollectInfo()這個方法:
public class TestCollectInfo implements ICollector {
@Override
public String collectInfo(Context context) {
return "這是一條測試采集異常信息";
}
}
由于每個項目不同,可能需要采集的異常信息外的其他一些手機信息都不盡相同,我這里在源碼中只設計了Key為deviceInfo和Key為exceptionInfo的兩種捕獲信息,deviceInfo主要是為了捕獲手機信息的而exceptionInfo就是捕獲異常崩潰信息的了,如果你的項目中還需要捕獲其他類型的信息,可以通過實現(xiàn)ICollector接口來定義自己想提交的采集信息即可,記得在初始化時調(diào)用.strategy(new TestCollectInfo(), "網(wǎng)絡屬性的Key")將采集信息傳入即可。
面臨的問題
在網(wǎng)上可以看到很多類似于全局捕獲異常發(fā)送服務器或者發(fā)送郵件給指定郵箱的功能,但是這些文章都沒有實際的深入場景,只是寫出了邏輯代碼,這樣就會面臨到一個很實際的問題:
異常發(fā)生時,我們要做的是將異常信息和一些其他捕獲到的手機信息或上傳服務器或通過郵件發(fā)送給指定郵箱,但是如果這個時間過長,導致App已經(jīng)退出,進程退出后,此進程的線程也不復存在,那么如果你要做的邏輯操作還沒做完,那么你這次異常的捕獲就是失敗的。
基于這個原因,我在異常發(fā)生時做的操作是這樣的:
- 捕獲異常并寫入本地異常捕獲文件;
- 給寫入文件的操作加入回調(diào)接口,告知主線程異常寫入完畢;
- 將異常信息、異常文件路徑、手機設備信息等參數(shù)傳入子進程的IntentService;
由于是子進程啟動的Service進行的業(yè)務邏輯操作,就算主進程已經(jīng)退出,也不會影響子進程的耗時操作,問題也就隨之解決了。
配套的Express文件
你可能會納悶了,什么是Express?這文件是干嘛的?
看過前面的部分后,你可能知道了這個組件是可以將異常信息發(fā)送給服務器的,而看這篇文章的很多可能都是移動端的開發(fā)人員,不一定懂服務端,就算懂,也未必能很快的搭建一個可以接受異常信息的服務端來測試,那么為了大家測試方便,我就把我的Express文件分享出來,如果你還不知道什么是Express或者Node.js,建議你先看這篇:
之后將你Nodejs根目錄下的app.js改為:
var fs = require('fs');
var path = require('path');
var express = require('express');
var bodyParser = require('body-parser');
var app = express();
var CRASH_FILE = path.join(__dirname, 'api/crashs.json'); // user.json文件的路徑
app.set('port', (process.env.PORT || 9999));
app.use('/', express.static(path.join(__dirname, 'public')));
//使用body-parser中間件
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(function(req, res, next) {
// Set permissive CORS header - this allows this server to be used only as
// an API server in conjunction with something like webpack-dev-server.
res.setHeader('Access-Control-Allow-Origin', '*');
// Disable caching so we'll always get the latest comments.
res.setHeader('Cache-Control', 'no-cache');
next();
});
//處理/api/crashs的POST請求
app.post('/api/crashs', function(req, res) {
fs.readFile(CRASH_FILE, function(err, data) {
if (err) {
console.error(err);
process.exit(1);
}
var crashs = JSON.parse(data);
//控制post提交的參數(shù)類型
var crash = {
deviceinfo: req.body.deviceInfo,
exceptioninfo: req.body.exceptionInfo,
testinfo:req.body.testInfo
};
//將user加入到users中去。
crashs.push(crash);
fs.writeFile(CRASH_FILE, JSON.stringify(crashs, null, 4), function(err) {
if (err) {
console.error(err);
process.exit(1);
}
//請求成功后返回的提示json
res.json("{code: 200, message: 'upload crash successful.'}");
});
});
});
app.listen(app.get('port'), function() {
console.log('Server started: http://localhost:' + app.get('port') + '/');
});
并且在同目錄的文件夾:api中放入crashs.json:
[
{
"deviceinfo": "手機信息異常===========================================<br>DISPLAY=Flyme 6.8.3.31R beta<br>REGION=CN<br>SERIAL=d4aa09c3<br>BOOTLOADER=unknown<br>SOFT_VERSION=Y.30<br>SUPPORTED_64_BIT_ABIS=[Ljava.lang.String;@e6de412<br>PERMISSIONS_REVIEW_REQUIRED=false<br>AUTO_TEST_ONEPLUS=false<br>ID=NMF26F<br>TAG=Build<br>HOST=xs-MacBookPro<br>TAGS=test-keys<br>TIME=1522481855000<br>TYPE=user<br>USER=xs<br>BOARD=QC_Reference_Phone<br>BRAND=OnePlus<br>MODEL=ONEPLUS A3010<br>RADIO=unknown<br>SUPPORTED_ABIS=[Ljava.lang.String;@833c7e3<br>MANUFACTURER=OnePlus<br>PRODUCT=OnePlus3<br>UNKNOWN=unknown<br>versionCode=1<br>versionName=1.0<br>IS_EMULATOR=false<br>FINGERPRINT=OnePlus/OnePlus3/OnePlus3T:7.1.1/NMF26F/builder.20180331153735_R:user/test-keys<br>HARDWARE=qcom<br>SUPPORTED_32_BIT_ABIS=[Ljava.lang.String;@b31279d<br>IS_BETA_ROM=true<br>CPU_ABI2=<br>CPU_ABI=arm64-v8a<br>IS_DEBUGGABLE=false<br>DEBUG_ONEPLUS=false<br>DEVICE=OnePlus3T<br>===========================================<br>",
"exceptioninfo": "Time:Fri May 10 14:23:32 GMT+08:00 2019 [Thread(id:3321, name:pool-2-thread-1, priority:5, groupName:main): LogCenter.java:184 run java.lang.RuntimeException: 測試CrashLib\n\tat com.dhcc.test.MainActivity$1.onClick(MainActivity.java:18)\n\tat android.view.View.performClick(View.java)\n\tat android.view.View$PerformClick.run(View.java:22549)\n\tat android.os.Handler.handleCallback(Handler.java:751)\n\tat android.os.Handler.dispatchMessage(Handler.java:95)\n\tat android.os.Looper.loop(Looper.java:154)\n\tat android.app.ActivityThread.main(ActivityThread.java)\n\tat java.lang.reflect.Method.invoke(Native Method)\n\tat com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)\n\tat com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)\n ] - 測試異常~~",
"testinfo": "這是一條測試采集異常信息"
}
]
然后啟動服務:
在Cmd下輸入:
> node app.js
之后在移動端,我們就可以設置成一下代碼來捕獲我們的異常信息了:
...
.setCrashServerUrl("http://你的ip:9999/api/crashs")
...
LogCenter.getLogCenter("com.dhcc.crashInfo", configuration)
//可以自定義異常 只要實現(xiàn)ICollector 并傳入網(wǎng)絡提交時所需要的key即可
.strategy(new TestCollectInfo(), "testInfo")
.init(this);
這里注意.strategy(new TestCollectInfo(), "testInfo")的testInfo,其實就是app.js中的req.body.testInfo和crashs.json中的testInfo字段。
整體設計架構(gòu)
源碼就不細說了,大家可以自己去看,有什么問題可以給我留言,謝謝你看完。