Android 综合技术

237 阅读6分钟

导语

本章主要讲解,CrashHandler来监视App的crash信息,通过Google的multiDex方案解决Android方法数超过65536的问题,Android动态加载dex,反编译。

主要内容

  • 使用CrashHandler来获取应用的crash信息
  • 使用multidex来解决方法数越界
  • Android动态加载技术
  • 反编译初步

具体内容

使用CrashHandler来获取应用的crash信息

如何检测崩溃并了解详细的crash信息? 首先需实现一个uncaughtExceptionHandler对象,在它的uncaughtException方法中获取异常信息并将其存储到SD卡或者上传到服务器中,然后调用Thread的setDefaultUncaughtExceptionHandler为当前进程的所有线程设置异常处理器。

CrashHandler源码

在Application初始化的时候为线程设置CrashHandler,这样之后,Crash就会通过我们自己的异常处理器来处理异常了。

public class BaseApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        CrashHandler crashHandler = CrashHandler.getInstance();
        crashHandler.init(this);
    } 
}

使用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年提出的解决方案.在Android5.0之前需要引入Google提供的android-support-multidex.jar;从5.0开始系统默认支持了multidex,它可以从apk文件中加载多个dex文件。

使用步骤:

  1. 修改对应工程目录下的build.gradle文件,在defaultConfig中添加multiDexEnabled true这个配置项。
  2. 在build.gradle的dependencies中添加multidex的依赖:compile ‘com.android.support:multidex:#1.0.0’
  3. 代码中加入支持multidex功能。 第一种方案,在manifest文件中指定Application为MultiDexApplication。 第二种方案,让应用的Application继承MultiDexApplication。 第三种方案,重写 attachBaseContext 方法,这个方法比onCreate还要先执行。
 public class BaseApplication extends Application {
     @Override
     protected void attachBaseContext(Context base) {
         super.attachBaseContext(base);
         MultiDex.install(this);
     }
 }

采用上面的配置项后,如果这个应用方法数没有越界,那么Gradle是不会生成多个dex文件的,当方法数越界后,Gradle就会在apk中打包2个或多个dex文件。当需要指定主dex文件中所包含的类,这时候就需要通过—multi-dex-list来选项来实现这个功能。

//在对应工程目录下的build.gradle文件,加入
afterEvaluate {
println "afterEvaluate"
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
def listFile = project.rootDir.absolutePath + '/app/maindexlist.txt'
println "root dir:" + project.rootDir.absolutePath
println "dex task found: " + dx.name
if (dx.additionalParameters == null) {
   dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += '--main-dex-list=' + listFile
dx.additionalParameters += '--minimal-main-dex'
}
} 

maindexlist.txt

com/ryg/multidextest/TestApplication.class
com/ryg/multidextest/MainActivity.class
// multidex 这9个类必须在主Dex中
android/support/multidex/MultiDex.class
android/support/multidex/MultiDexApplication.class
android/support/multidex/MultiDexExtractor.class
android/support/multidex/MultiDexExtractor$1.class
android/support/multidex/MultiDex$V4.class
android/support/multidex/MultiDex$V14.class
android/support/multidex/MultiDex$V19.class
android/support/multidex/ZipUtil.class
android/support/multidex/ZipUtil$CentralDirectory.class

需要注意multidex的jar中的9个类必须要打包到主dex中,因为Application的attachBaseContext方法中需要用到MultiDex.install(this)需要用到MultiDex。

Multidex的缺点:

  1. 启动速度会降低,由于应用启动时会加载额外的dex文件,这将导致应用的启动速度降低,甚至产生ANR现象。
  2. 因为Dalvik linearAlloc的bug,可以导致使用multidex的应用无法在Android4.0之前的手机上运行,需要做大量兼容性测试。

Android动态加载技术

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

学习一下作者的插件化开源框架:dynamic-load-apk

各种插件化方案都需要解决3个基础性问题

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

资源访问 插件中凡是以R开头的资源文件都不能访问。 Activity的工作主要是通过ContextImpl完成的,Activity中有一个mBase的成员变量,它的类型就是ContextImpl。Context有两个获取资源的抽象方法getAsssets()和getResources();只要实现这两个方法就可以解决资源问题。

protected void loadResources() {  
    try {  
        AssetManager assetManager = AssetManager.class.newInstance();  
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);  
        addAssetPath.invoke(assetManager, mDexPath);  
        mAssetManager = assetManager;  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    Resources superRes = super.getResources();  
    mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),  
            superRes.getConfiguration());  
    mTheme = mResources.newTheme();  
    mTheme.setTo(super.getTheme());  
}

从loadResources()的实现看出,加载资源的方法是反射,通过调用AssetManager的addAssetPath方法,我们可以将一个apk中的资源加载到Resources对象中。传递的路径可以是zip或资源目录,因此直接将apk的路径传给它,资源就加载到AssetManager了。然后再通过AssetManager创建一个新的Resources对象,通过这个对象就可以访问插件apk中的资源了。

接着在代理Activity中实现getAssets()和getResources()。关于代理Activity参考作者的插件化开源框架。

@Override  
public AssetManager getAssets() {  
    return mAssetManager == null ? super.getAssets() : mAssetManager;  
}  
 
@Override  
public Resources getResources() {  
    return mResources == null ? super.getResources() : mResources;  
}

Activity的生命周期管理 为什么会有这个问题,其实很好理解,apk被宿主程序调起以后,apk中的activity其实就是一个普通的对象,不具有activity的性质,因为系统启动activity是要做很多初始化工作的,而我们在应用层通过反射去启动activity是很难完成系统所做的初始化工作的,所以activity的大部分特性都无法使用包括activity的生命周期管理,这就需要我们自己去管理。

详细内容参考作者的开源框架介绍

ClassLoader的管理 为了避免多个ClassLoader加载了同一个类所引发的类型转换错误。将不同插件的ClassLoader存储在一个HashMap中。

反编译初步

  1. 使用dex2jar和jd-gui反编译apk
  2. 使用apktool对apk进行二次打包 以上网上资料特别多,不赘述。