一、异常收集
目的:在APP上线后,可能会出现一些BUG导致了APP的闪退,用户体验就非常致命,我们一定要第一时间找到问题的所在,去处理掉问题,处理有方法有两种,一是发一个修改后的新版本,另一个是用热修复发布一个更新补丁,具体选择哪一种符合自己需求就行。
我们主要说的异常的收集和处理,热修复不在范畴内。
1、我们需要自定义一个异常收集类(创建一个Thread.UncaughtExceptionHandler的继承类);
2、替换掉APP本身的异常处理(在Thread.UncaughtExceptionHandler实现类中使用Thread.setDefaultUncaughtExceptionHandler(this)方法替换);
3、在类中收集信息,这个信息最好包括手机的一些信息,比如:厂商、型号、cup型号、内存大小等...,因为安卓手机的多样性导致我们在适配的时候非常麻烦,也是因为有些问题的出现是因为个别的硬件差异造成的,所以这些信息最好收集;
整体思路就是,自定义一个异常收集类替换到本来的异常处理类,在这个类中去收集一些必要的信息回传到我们的后台,我们可以在崩溃信息发生的第一时间去处理
以下是异常收集类代码,可以用作参考,也可以直接用(这个类是参考的别人的,自己做了一些修改)
public class CrashHandlerUtil implements Thread.UncaughtExceptionHandler {
public static final String TAG = "CrashHandlerUtil";
//系统默认的UncaughtException处理类
private Thread.UncaughtExceptionHandler mDefaultHandler;
//CrashHandler实例
private static CrashHandlerUtil INSTANCE = new CrashHandlerUtil();
//程序的Context对象
private Context mContext;
//用来存储设备信息和异常信息
private Map<String, String> infos = new HashMap<>();
//用于格式化日期,作为日志文件名的一部分
private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA);
private String crashTip = "应用开小差了,稍后重启下,亲!";
public String getCrashTip() {
return crashTip;
}
public void setCrashTip(String crashTip) {
this.crashTip = crashTip;
}
private CrashHandlerUtil() {
}
public static CrashHandlerUtil getInstance() {
return INSTANCE;
}
public void init(Context context) {
mContext = context;
//获取系统默认的UncaughtException处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//设置该CrashHandler为程序的默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当UncaughtException发生时会转入该函数来处理
*
* @param thread 线程
* @param ex 异常
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
//如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Logger.e("error : ", e);
e.printStackTrace();
}
//退出程序
//退出JVM(java虚拟机),释放所占内存资源,0表示正常退出(非0的都为异常退出)
System.exit(0);
//从操作系统中结束掉当前程序的进程
android.os.Process.killProcess(android.os.Process.myPid());
}
}
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param throwable 异常
* @return true:如果处理了该异常信息;否则返回false.
*/
private boolean handleException(final Throwable throwable) {
if (throwable == null) {
return false;
}
//使用Toast来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
throwable.printStackTrace();
StringUtils.showMsgAsCenter(mContext,getCrashTip());
Looper.loop();
}
}.start();
//收集设备参数信息
collectDeviceInfo(mContext);
//保存日志文件
saveCrashInfo2File(throwable);
return true;
}
/**
* 收集设备参数信息
*
* @param ctx 上下文
*/
public void collectDeviceInfo(Context ctx) {
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = pi.versionCode + "";
infos.put("versionName", versionName);
infos.put("versionCode", versionCode);
}
} catch (PackageManager.NameNotFoundException e) {
Logger.e("an error occured when collect package info", e);
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
// Logger.e(field.getName() + " : " + field.get(null));
} catch (Exception e) {
Logger.e("an error occured when collect crash info", e);
}
}
}
/**
* 保存错误信息到文件中
*
* @param ex 异常
* @return 返回文件名称, 便于将文件传送到服务器
*/
private String saveCrashInfo2File(Throwable ex) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
sb.append(result);
Logger.e(sb.toString());
if(BuildConfig.DEBUG) {
return null;
}
/*
这个 crashInfo 就是我们收集到的所有信息,可以做一个异常上报的接口用来提交用户的crash信息
*/
String crashInfo = sb.toString();
return null;
}
}
Application的onCreate中调用init()方法
收集的信息结构如下
SUPPORTED_64_BIT_ABIS=[Ljava.lang.String;@f4d714
versionCode=54
BOARD=unknown
BOOTLOADER=unknown
TYPE=user
ID=MRA58K
TIME=1506044459000
BRAND=Xiaomi
TAG=Build
SERIAL=7D6TPFT4QCS8S8FQ
HARDWARE=mt6797
SUPPORTED_ABIS=[Ljava.lang.String;@f3fdbbd
CPU_ABI=armeabi-v7a
RADIO=unknown
IS_DEBUGGABLE=false
DISPLAY_TYPE=unknown
MANUFACTURER=Xiaomi
SUPPORTED_32_BIT_ABIS=[Ljava.lang.String;@3f3dd67
TAGS=release-keys
CPU_ABI2=armeabi
UNKNOWN=unknown
USER=builder
FINGERPRINT=Xiaomi/nikel/nikel:6.0/MRA58K/V8.5.7.0.MBFCNED:user/release-keys
HOST=c3-miui-ota-bd06.bj
PRODUCT=nikel
versionName=1.4.7
DISPLAY=MRA58K
MODEL=Redmi Note 4
DEVICE=nikel
java.lang.OutOfMemoryError: Failed to allocate a 843012 byte allocation with 381736 free bytes and 368KB until OOM
at com.bumptech.glide.b.a.a(SourceFile:379)
at com.bumptech.glide.load.resource.c.b.<init>(SourceFile:92)
at com.bumptech.glide.load.resource.c.b$a.newDrawable(SourceFile:368)
at com.bumptech.glide.load.resource.a.a.a(SourceFile:32)
at com.bumptech.glide.load.resource.a.a.b(SourceFile:16)
at com.bumptech.glide.load.engine.g.b(SourceFile:44)
at com.bumptech.glide.request.GenericRequest.a(SourceFile:487)
at com.bumptech.glide.load.engine.c.b(SourceFile:158)
at com.bumptech.glide.load.engine.c.a(SourceFile:22)
at com.bumptech.glide.load.engine.c$b.handleMessage(SourceFile:202)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5791)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762)
收集到了一个OOM,这个可能也比较常见了,我们在处理一些大图片的时候,如果稍不注意可能就会造成OOM了,特别是现在的一些低配手机上。
当你看到这个异常信息后是不是对怎么处理就心有成竹了呢。
那么问题又来了,这个收集到的信息怎么都是a.b.c这样的?
大家在发布APP的时候肯定都对代码进行过混淆,混淆后的代码就是这样,那么收集的时候也只能收集到这个程度了。
二、如何处理混淆后的异常信息
我们可以利用SDK中tools下的proguardgui.bat工具和混淆对应文档进行反混淆处理
D:\Android\sdk\tools\proguard\bin\proguardgui.bat 工具在SDK中的位置,有的SDK版本不同这个工具的具体位置可能有改变,也可以在tools中直接搜索proguardgui.bat,双击运行即可
1、点击左侧栏中的Retrace
2、mapping file处选择APP的mapping文件的位置
3、Obfuscated stack trace输入你收集到的异常信息,注意是异常信息,并不是我们刚才收集的那些所有的信息,刚才收集的信息中还包含了手机的一些信息,这些不需要,只复制这些到输入框
java.lang.OutOfMemoryError: Failed to allocate a 843012 byte allocation with 381736 free bytes and 368KB until OOM
at com.bumptech.glide.b.a.a(SourceFile:379)
at com.bumptech.glide.load.resource.c.b.<init>(SourceFile:92)
at com.bumptech.glide.load.resource.c.b$a.newDrawable(SourceFile:368)
at com.bumptech.glide.load.resource.a.a.a(SourceFile:32)
at com.bumptech.glide.load.resource.a.a.b(SourceFile:16)
at com.bumptech.glide.load.engine.g.b(SourceFile:44)
at com.bumptech.glide.request.GenericRequest.a(SourceFile:487)
at com.bumptech.glide.load.engine.c.b(SourceFile:158)
at com.bumptech.glide.load.engine.c.a(SourceFile:22)
at com.bumptech.glide.load.engine.c$b.handleMessage(SourceFile:202)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:207)
at android.app.ActivityThread.main(ActivityThread.java:5791)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:901)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762)
4、最后点击ReTrace
详细的异常信息就看到了。
大家看到了是加载gif图片时发生了OOM,接下来就可以根据异常信息去改写代码了。然后修复APP吧
最后把这几个文件描述下,作为参考,赶紧点开你的工程的找个文件看看到底是什么东东
dump.txt
APK文件中所有类的内部结构
mapping.txt
混淆前后类、方法、类成员等的对照
resources.txt
工程中用到的所有资源信息(描述可能不完全)
seeds.txt
没有被混淆的类和成员
usage.txt
被移除的代码
如有什么问题,欢迎留言交流,共同提高。