《Android 开发艺术探索》 13-综合技术

抄书系列

Posted by Suzeyu on 2016-08-25

crash的异常处理, Android中的方法限制, 反编译等也是需要了解的技术.

blog相关代码

捕捉Crash信息

应用总会不可避免的发生Crash, 有可能是因为底层bug, 或者是适配问题, 代码逻辑问题等等. 当发生Crash的时候, 系统会kill掉正在执行的程序, 现象就是闪退或者提示用户程序已经停止运行, 这对用户来说是很不友好, 也是开发者不愿意看到的. 系统提供了在程序Crash之前可以保存异常信息的能力, 这样在崩溃前可以保存异常信息到文件中, 传回服务器,这样可以针对各种各样的用户发生的错误对应用进行完善的方法, 这个类在Thread中的setDefaultUncaughtException()方法.

/**
* Sets the default uncaught exception handler. This handler is invoked in
* case any Thread dies due to an unhandled exception.
*
* @param handler The handler to set or null.
*/
public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler handler) {
Thread.defaultUncaughtHandler = handler;
}

从方法名的意思看出设置默认未捕获异常的处理器. 就是当Crash发生的时候, 系统会回调UncaughtExceptionHandler#uncaughtException()方法, 在这个方法中可以获取到异常的信息, 可以选择把信息写到SD卡中, 然后在合适的时机通过网络将Crash信息上传到服务器上. 也可以在这里弹出一个对话框告诉用户, 会比直接闪退好那么一点.

首先要实现出一个UncaughtExceptionHandler的子类. 直接看实现:

public class MyCrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = MyCrashHandler.class.getSimpleName();
/**
* 定义异常文件要存储的路径
*/
private static final String PATH = Environment.getExternalStorageDirectory().getPath() + "/szyCrash/log/";
private static final String FILE_NAME = "crash-";
private static final String FILE_NAME_SUFFIX = ".trace";
private Thread.UncaughtExceptionHandler mDefaultUncaughtExceptionHandler;
private Context mContext;
/**
* 单例模式三部曲 懒汉式
*/
private static MyCrashHandler sInstance = new MyCrashHandler();
private MyCrashHandler() {
}
public static MyCrashHandler getsInstance() {
return sInstance;
}
/**
* 设置本类为当前进行的默认未捕捉异常处理器
*/
public void init(Context context) {
mContext = context.getApplicationContext();
mDefaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
try {
// 导出信息到sd卡上
dumpExceptionToSDCard(ex);
// 对本地的信息进行上传服务器处理
uploadExceptionToServer();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 也把异常打印到控制台 防止系统没有默认异常处理器导致的没有崩溃信息
ex.printStackTrace();
// 如果系统有默认的异常处理器那么就给系统处理 否则自己关闭掉
// 如果不交给系统 或者 自己手动杀掉, 那么应用就会进入假死, 点击会出现ANR
if (mDefaultUncaughtExceptionHandler != null) {
ex.printStackTrace();
mDefaultUncaughtExceptionHandler.uncaughtException(thread, ex);
} else {
Process.killProcess(Process.myPid());
}
}
}
/**
* 上传发生未捕获的错误日志
*/
private void uploadExceptionToServer() {
// TODO: 16/8/25 看具体需求, 也可以在每次启动app后检测本地是否有异常日志
}
/**
* 开始异常信息进行本地存储处理
*
* @param ex 程序发生的异常对象
* @throws IOException 写入文件发生异常
*/
private void dumpExceptionToSDCard(Throwable ex) throws IOException {
// 首先判断sd卡是否被挂起
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Log.w(TAG, "sd卡不可用, 跳过 dump Exception");
return;
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
// 获取日志记录时间
long currentTimeMillis = System.currentTimeMillis();
String formatTimeStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(currentTimeMillis));
// 创建存储异常的文件
File file = new File(PATH + FILE_NAME + formatTimeStr + FILE_NAME_SUFFIX);
try {
// 创建写入流, 开始哗哗写东西
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(formatTimeStr);
dumpPhoneInfo(pw);
pw.println();
pw.println("**********************异常信息**************************");
ex.printStackTrace(pw);
pw.close();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "存储未捕获异常失败了.");
}
}
/**
* 读取当前设备信息 并追加到异常日志中
*/
private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
// 获得应用包管理者 并获取存储当前应用的信息对象
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.println("**********************设备信息**************************");
// 开始写入 应用信息
pw.println("App Version ");
pw.print(" VersionName: ");
pw.println(pi.versionName);
pw.print(" VersionCode: ");
pw.println(pi.versionCode);
// 开始写入 Android版本信息
pw.println("OS Version ");
pw.println(" SDK_NAME: " + Build.VERSION.RELEASE);
pw.println(" SDK_INT: " + Build.VERSION.SDK_INT);
// 开始写入 手机制造商
pw.println("Vendor ");
pw.println(" " + Build.MANUFACTURER);
// 开始写入 手机型号
pw.println("Model ");
pw.println(" " + Build.MODEL);
// 开始写入 CPU架构
pw.println("CPU ABI ");
String[] supportedAbis = SUPPORTED_ABIS;
for (int i = 0; i < supportedAbis.length; i++) {
pw.println(" " + supportedAbis[i]);
}
pw.println("*************************end***************************");
}
}

核心方法就是复写了这个类的uncaughtException()方法. 别忘了在Application中进行这个类的init()方法调用. 这样才会把自定义的捕捉未捕获的异常这个类注册到系统.

看一下模拟机和真机的两个log日志


multidex解决方法数越界

Android中单个dex文件所能包含的最大方法数为65536, 这包含了FrameWork, 依赖的jar包以及应用本身的代码中的所有方法. 会爆出:

com.android.dex.DexIndexOverflowException: method ID not in[0, 0xffff] :65536

可能在一些低版本的手机, 即使没有超过方法数的上限却还是出现错误

E/dalvikvm: Optimization failed
E/installd: dexopt failed on '/data/dalvik-cache/.....'

这个现象, 首先dexpot是一个程序, 应用在安装时, 系统会通过dexopt来优化dex文件, 在优化过程中dexopt采用一个固定大小的缓冲区来存储应用中所有方法消息, 这个缓冲区就是linearAlloc. LinearAlloc缓冲区在新版本的Android系统中大小为8MB或者16MB. 在Android 2.2和2.3中却只有5MB. 这是如果方法过多, 即使方法数没有超过65535也有可能会因为存储空间失败而无法安装.

解决方案

  • 插件化: 是一套重量级的技术方案, 通过将一个dex拆分成两个或者多个dex,可以在一定程度上解决方法数的越界问题. 但是还有兼容性问题需要考虑, 所以需要权衡是否需要使用这个方案.
  • multidex: 这是Google在2014年提出的解决方案

= =. 略过

Android的动态加载技术

动态加载也叫插件化. 当项目越来越大的时候, 可以通过插件化来减轻应用的内存和CPU占用. 还可以实现热插播, 即可以在不发布新版本的情况下更新某些模块.

插件化基本都要面临解决的基本问题:

  1. 资源访问
  2. Activity声明周期的管理
  3. ClassLoader的管理

说明宿主插件的概念, 宿主是指普通的apk, 而插件一般指经过处理的dex或者apk. 在主流的插件化框架中多采用经过处理的apk来作为插件, 处理方式往往和编译以及打包环节有关, 另外很多插件化框架都需要用到代理Activity的概念, 插件Activity的启动大多数是借助一个代理Activity来实现.

= =. 略过

反编译

= =. 略过

第14章: JNI和NDK编程