(转载)Building Apps with Over 65K Methods -- 你可能会遇到的Class Not Found问题

本文转载于:罗升阳的新浪微博  http://weibo.com/p/1001603884849646685222   

尊重原创,转载一定要加上原创地址!

      

随着功能越来越多,APK包含的方法数越来越多,一旦超过65K,就Game Over了。怎么解决这个问题呢?

官方提供了Multidex方案,即将APK的方法打包在不同的Dex文件中,具体方法可以参考官方文档:https://developer.android.com/tools/building/multidex.html。其中,用于启动APKDex称为Main Dex,其余的Dex称为AdditionalDex

使用了MultidexAPK运行在Android5.0之前的设备上时,还需要配合support库里面的MultiDex.install接口才行。有三种方法使用MultiDex.install接口:

1.      如果没有自定义自己的Application,那么在AndroidManifest.xmlAPKApplication指定为MultiDexApplication

2.      如果自定义了自己的Application,那么将自己的Application继承于MultiDexApplication

3.      如果不想继承于MultiDexApplication,那么重写父类Applicatio的成员函数attachBaseContext,并且在该成员函数中调用MultiDex.install接口。

从第3点可以判断出,MultiDexApplication所干的事情其实也重写了父类Application的成员函数attachBaseContext,并且在该成员函数中调用了MultiDex.install接口。

Application. attachBaseContext是什么时候调用的呢?APK启动的时候,首先加载的是MainDex,并且从中创建一个Application对象。注意,如果自定了Application,那么创建的自定义的Application对象。接着就会调用该Application对象的成员函数attachBaseContext。再接下来Application对象的成员函数onCreate才会被调用。

MultiDex.install干了什么事情呢?它首先是收集APK里面的Additional Dex,并且找到APK所使用的PathClassLoader。接下来通过反射得到PathClassLoader的成员变量pathList,这是一个类型为DexPathList的对象。DexPathList里面又有一个成员变量dexElements,指向的是一个Element数组,该数组包含了系统主动为APK加载的Dex。再接下来,又通过反射调用上述的DexPathList对象的成员函数makeDexElements加载前面找到的Additional Dex,并且将这些Additional Dex增加到它的成员变量dexElements描述的Element数组中。

Dalvik虚拟机在查找一个Class的时候,会询问APK使用的PathClassLoaderPathClassLoader又询问它的成员变量pathList指向的DexPathList对象。DexPathList又询问保存在它的成员变量dexElements描述的一个Element数组中的Dex。因此,就可以想象中,一旦MultiDex.install调用过后,APK就可以正常使用打包在AdditionalDex中的Class

现在问题就来了,在MultiDex.install调用之前执行的代码,是不允许引用在打包在Additional Dex中的Class的。一旦引用了,在APK执行期间,就会出现Class Not Found异常。这篇文档https://github.com/casidiablo/multidex,给出了一个解决方案,避免上述Class Not Found问题:

1.       The static fields in yourapplication class will be loaded before the MultiDex#installbe called! So thesuggestion is to avoid static fields with types that can be placed out of mainclasses.dex file.

2.       The methods of yourapplication class may not have access to other classes that are loaded afteryour application class. As workarround for this, you can create another class(any class, in the example above, I use Runnable) and execute the methodcontent inside it. Example:

public class XXXXXX extends Application    

   @Override

    publicvoid onCreate() {

       super.onCreate();

 

       final Context mContext = this;

        newRunnable() {

            @Override

           public void run() {

               // put your logic here!

               // use the mContext instead of this here

           }

       }.run();

    }

}

1点好理解,不重述。第2点什么鬼意思呢?我们假设我们的代码原来是这样:

public class XXXXXX extends Application    

   @Override

    publicvoid onCreate() {

       super.onCreate();

        EventBus.getDefault().register(this);

    }

}

 

         注意,粗红斜体那行代码。我们假设EventBus这个Class打包在 Additional Dex中。这时候在APK启动时,就会出现EventBus Not Found的异常。

         如果改成以下的代码,那么就可以避免出现EventBus Not Found的异常:

public class XXXXXX extends Application    

   @Override

    publicvoid onCreate() {

       super.onCreate();

                   newRunnable() {

           public void run() {

                EventBus.getDefault().register(this);

            }

}.run();

 }

}

这是什么原理呢?网上找不到答案?那我们就自力更生,丰衣足食吧。

首先要回答的一个问题是为什么会抛出异常。这得从APK安装开始说起。APK在安装的时候,会通过工具dexopt进行优化。在优化之前,又会首先验证指令流的合法性。在Android 5.0之前,只会验证APK里面的Main Dex的方法。

没有使用Runnable的代码,XXXXXX::onCreate引用了打包在AdditionalDex中的EventBus,很显然这是一个非法指令。

使用了Runnable的代码,会生成一个匿名类XXXXXX$1,它继承了Runnable类。XXXXXX$1::run也引用了打包在AdditionalDex中的EventBus,很显然这也是一个非法指令。

因此这两个类都会在优化后的Dex中标记为还没有验证通过,即其状态被标记为Not Verified。标记为Not VerifiedClass,在运行时需要用到时,要再次进行指令流合法性验证。

没有使用Runnable的代码,XXXXXX类是在MultiDex.install之前使用的,也就是在MultiDex.install之前被Dalvik加载。Dalvik发现它标记为Not Verified,于是就会对它的所有方法的指令进行合法性验证。这时候结果仍是一样的,没有办法找到XXXXXX::onCreate引用的EventBus。这时候Dalvik会干一件事情,将调用EventBus::getDefault的指令修改为一个会抛出异常的指令。所以等到XXXXXX::onCreate调用的时候,尽管这时候MultiDex.install已经调用过了,它仍然会抛出一个异常,说EventBus Not Found

使用了Runnable的代码,EventBus::getDefault是在XXXXXX$1::run引用的,而XXXXXX$1::run又是在XXXXXX::onCreate引用的。但是请注意,XXXXXX::onCreate是通过Runnable::run来调用XXXXXX$1::run的。这意味着XXXXXX::onCreate引用的是Runnable::run,而不是XXXXXX$1::run。这样,当XXXXXX::onCreate被验证时,是可以Pass的。

请仔细想一想为什么在验证XXXXXX:: onCreate时,为什么不能认为它引用了XXXXXX$1::run?这是因为我们创建的是一个XXXXXX$1对象,但是却赋值给了一个Runnable变量。当调用这个Runnable变量的成员函数run时,使用的是invoke-virtual指令。Dalvik验证invoke-virtual指令时,只能使用this指针的静态类型!因为只有在真正运行时,才能确定this的具体类型!这一点看不明白的,需要自行脑补一下。

这意味着,使用了Runnable的代码,在调用MultiDex.install之前,涉及到的类只有XXXXXX,而这时候并没有引用打包在AdditionalDex中的Class,因此就一切正常。等到XXXXXX::onCreate被调用的时候,它确定了Runnable::run实际上是XXXXXX$1::run。这时候就会在已经加载的Dex中查找XXXXXX$1。这次很幸运,在Additional Dex中找到了,于是就可以愉快地执行下去了!

因此,上述提到的第2个注意点,实际上是通过一个中间类将对打包在的 Additional Dex中的Class的引用指令延迟到 MultiDex.install之后才去验证,因此就可以避免抛出Class Not Found异常!

最后,使用了MultidexAPK运行在Android5.0之后的设备上时,不需要MultiDex.install支持。这是因为Android 5.0使用的是ART虚拟机,ART虚拟机解决了Dalvik虚拟机方法数限制在65K的问题。Android 5.0在安装一个使用了MultiDexAPK时,会收集它的Main DexAdditional Dex,然后将它们翻译成Native Code,最终保存在一个OAT文件中。因此就不需要MultiDex.install了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值