作为一个android开发,经常遇到crash情况。原因各种各样,即使是经过了测试的大量检测,但是到用户手上还是会遇到闪退。这和android设备的碎片化有关,也和使用时的环境有关,比如弱网,比如高铁频繁切换小区等等。然而我们不能让用户帮我们抓取log,那要怎么才能知道为什么闪退了呢?
幸运的是:安卓已经帮我们想好了解决问题的接口(UncaughtExceptionHandler
)。从名称上就知道这是用来处理没有捕捉到的野生Exception的。平时我们try catch的Exception的那就叫捕捉到的。看一下UncaughtExceptionHandler
的源码:
public interface UncaughtExceptionHandler {
/**
* Method invoked when the given thread terminates due to the
* given uncaught exception.
* Any exception thrown by this method will be ignored by the
* Java Virtual Machine.
* @param t the thread
* @param e the exception
*/
void uncaughtException(Thread t, Throwable e);
}
代码很简单,这是一个接口,并且只有一个方法。当出现未捕捉的exception时,系统会回调这个方法。具体实现在ThreadGroup.java
的uncaughtException
:
public void uncaughtException(Thread t, Throwable e) {
if (parent != null) {
parent.uncaughtException(t, e);
} else {
Thread.UncaughtExceptionHandler ueh =
Thread.getDefaultUncaughtExceptionHandler(); //获取当前默认的UncaughtExceptionHandler
if (ueh != null) {
ueh.uncaughtException(t, e); //此处回调uncaughtException()方法
} else if (!(e instanceof ThreadDeath)) {
System.err.print("Exception in thread \""
+ t.getName() + "\" ");
e.printStackTrace(System.err);
}
}
}
从上面的源码分析我们知道,只要我们重写一个类实现UncaughtExceptionHandler接口,替换当前线程的默认UncaughtExceptionHandler对象就行。系统为我们提供了方法Thread.getDefaultUncaughtExceptionHandler()
和Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler u)
。
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = CrashHandler.class.getSimpleName();
private static CrashHandler INSTANCE = new CrashHandler();
private Context mContext;
private Thread.UncaughtExceptionHandler mDefaultExceptionHandler;
@Override
public void uncaughtException(Thread t, Throwable e) {//当发生exception时候会回调该方法
dumpToSDCard(t, e);//dump trace 信息到sd卡
//todo 上传服务器
e.printStackTrace();
if (mDefaultExceptionHandler != null) { //交给系统的UncaughtExceptionHandler处理
mDefaultExceptionHandler.uncaughtException(t, e);
} else {
Process.killProcess(Process.myPid()); //主动杀死进程
}
}
private CrashHandler() {
}
public static CrashHandler getInstance() {
return INSTANCE;
}
public void init(Context context) {
this.mContext = context;
mDefaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();//获取当前默认ExceptionHandler,保存在全局对象
Thread.setDefaultUncaughtExceptionHandler(this);//替换默认对象为当前对象
}
private void dumpToSDCard(final Thread t, final Throwable e) {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.i(TAG, "no sdcard skip dump ");
return;
}
String mLodPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/crashHandler/";
File file = new File(mLodPath);
if (!file.exists()) {
file.mkdirs();
}
String time = new SimpleDateFormat("yyyy-mm-dd-HH:mm:ss", Locale.CHINA).format(new Date(System.currentTimeMillis()));
Log.i(TAG, mLodPath + time + ".trace");
File logFile = new File(mLodPath, time + ".trace");
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(logFile)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
e.printStackTrace(pw);
pw.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
private void dumpPhoneInfo(PrintWriter pw) {
//应用的版本名称和版本号
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = null;
try {
pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
pw.print("App Version: ");
pw.print(pi.versionName);
pw.print('_');
pw.println(pi.versionCode);
//android版本号
pw.print("OS Version: ");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.println(Build.VERSION.SDK_INT);
//手机制造商
pw.print("Vendor: ");
pw.println(Build.MANUFACTURER);
//手机型号
pw.print("Model: ");
pw.println(Build.MODEL);
//cpu架构
pw.print("CPU ABI: ");
pw.println(Build.CPU_ABI);
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
private void zip(String src, String dest) throws IOException { //压缩文件夹,为上传做准备。节省流量。
ZipOutputStream out = null;
File outFile = new File(dest);
File fileOrDirectory = new File(src);
out = new ZipOutputStream(new FileOutputStream(outFile));
if (fileOrDirectory.isFile()) {
zipFileOrDirectory(out, fileOrDirectory, "");
}else {
File[] entries = fileOrDirectory.listFiles();
for (int i = 0; i < entries.length; i++) {
zipFileOrDirectory(out, entries[i], "");
}
}
if(null != out){
out.close();
}
}
private static void zipFileOrDirectory(ZipOutputStream out,File fileOrDirectory, String curPath) throws IOException {
FileInputStream in = null;
if (!fileOrDirectory.isDirectory()){
byte[] buffer = new byte[4096];
int bytes_read;
in = new FileInputStream(fileOrDirectory);
ZipEntry entry = new ZipEntry(curPath + fileOrDirectory.getName());
out.putNextEntry(entry);
while ((bytes_read = in.read(buffer)) != -1) {
out.write(buffer, 0, bytes_read);
}
out.closeEntry();
}else{
File[] entries = fileOrDirectory.listFiles();
for (int i = 0; i < entries.length; i++) {
zipFileOrDirectory(out, entries[i], curPath + fileOrDirectory.getName() + "/");
}
}
if (null != in){
in.close();
}
}
}
代码比较简单,就是替换当前线程默认的UncaughtExceptionHandler为我们自己实现的handler,当出现exception时候保存信息到sd卡,在上传服务器(还未实现 >_<||| )。为了节省流量,可以选择打包文件。
实现了之后怎么使用?
只需要在 application 的 onCreate() 中进行初始化
public void onCreate() {
super.onCreate();
CrashHandler.getInstance().init(this);
}
到这里基本就ok了。其实这些代码网上很多人都写过,我重写一遍加深记忆。但是很多人写到这就完成了,我想可能他们没有具体测试过,我自己实现了发现了不少问题。
上面初始化之后,我们手贱的自己去抛个exception。
随便找个地儿来一句 throw new RuntimeException("测试Crash");
然后坐等生效。然而事实总是爱打脸。接下来说一下遇到的问题。
if (!file.exists()) {
file.mkdirs();
}
于是我打印了mkdirs()返回值,是false 创建失败。我想到了权限,看了 mainfast 有声明:WRITE_EXTERNAL_STORAGE。并且思丢丢也没有打印permission denied。而是报错No such file or directory。这是我打开了百度,一番搜索之后还是权限问题:6.0之后的动态申请权限。
private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static String[] PERMISSIONS_STORAGE = {
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE" };
public static void verifyStoragePermissions(Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
try {
//检测是否有写的权限
int permission = ActivityCompat.checkSelfPermission(activity,
"android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
// 没有写的权限,去申请写的权限,会弹出对话框
ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
申请之后,完美!文件可以生成了。
2. adb找不到文件,无法pull出来
文件生成之后,我们打开文件管理,看到crashHandler文件夹和里面的trace文件。把手机插到电脑上,用电脑的文件管理器访问sd卡,找不到crashHandler文件夹。。。。刷新插拔都找不到。不怕,我还有其他技能,adb命令。
adb shell
//ok 文件存在
bullhead:/ $ cd sdcard/cr
crashHandler/ crash_cache/
bullhead:/ $ cd sdcard/crashHandler/
bullhead:/sdcard/crashHandler $ ls
2018-43-14-09:43:43.trace
bullhead:/sdcard/crashHandler $ exit
//pull出来就行,但是找不到,刚刚不是还在,为啥?
adb pull /sdcard/crashHandler/2018-43-14-09:43:43.trace
adb: error: cannot create '.\2018-43-14-09:43:43.trace': No such file or directory
adb pull /sdcard/crashHandler
adb: error: cannot create '.\crashHandler\2018-43-14-09:43:43.trace': No such file or directory
上面这个问题折腾了好久,终于在一篇文章上找到了问题。安卓只在开机时候会扫描文件结构,之后不会主动去扫描,只有通知它扫描某个文件,它才会扫描新的文件加入到文件结构中。所以我就需要主动去驱动扫描新文件。(估计做图库的同学比较懂)
String mLodPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/crashHandler/";
MediaScannerConnection.scanFile(this.getApplicationContext(), new String[]{ mLodPath }, null, new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(final String path, final Uri uri) {
Log.i(TAG, "MediaScannerConnection ionScanCompleted ");
}
});
到此没毛病。其实还有简单方法就是重启手机,这样也可以解决。。这个问题如果不是要电脑查看是不会遇到的,因为代码是可以访问这个文件的,所以加不加无所谓。
贴一下文件内容:
2018-21-14-10:21:41
App Version: 1.0_1
OS Version: 7.1.1_25
Vendor: LGE
Model: Nexus 5X
CPU ABI: arm64-v8a
java.lang.RuntimeException: 测试Crash
at com.lw.wanandroid.MainActivity.onNavigationItemSelected(MainActivity.java:56)
at android.support.design.widget.BottomNavigationView$1.onMenuItemSelected(BottomNavigationView.java:184)
at android.support.v7.view.menu.MenuBuilder.dispatchMenuItemSelected(MenuBuilder.java:822)
at android.support.v7.view.menu.MenuItemImpl.invoke(MenuItemImpl.java:156)
at android.support.v7.view.menu.MenuBuilder.performItemAction(MenuBuilder.java:969)
at android.support.design.internal.BottomNavigationMenuView$1.onClick(BottomNavigationMenuView.java:90)
at android.view.View.performClick(View.java:5637)
at android.view.View$PerformClick.run(View.java:22429)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6119)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
以上就是crash收集的简单实现,复杂的话,可以看不少公司都有相关的运营统计的sdk方案,比腾讯的bugly。
同样在此感谢《Android开发艺术探索》一书的作者,普及很多基础知识。