[关闭]
@946898963 2020-07-15T17:50:56.000000Z 字数 24100 阅读 2589

MultiDex深入学习

Android热修复


产生背景

在Android中单个dex文件所能够包含的最大方法数为65536,这包含Android FrameWork、依赖的jar包以及应用本身的代码中的所有方法。65536是一一个很大的数,一般来说一个简单应用的方法数的确很难达到65536,但是对于一-些比较大型的应用来说,65536就很容易达到了。当应用的方法数达到 65536 后,编译器就无法完成编译工作并抛出类似下面的异常:

  1. UNEX PECTED TOP-LEVEL EXCEPT ION:
  2. com. Android. Dex. DexIndexOverflowException: method ID not in [0Oxffff]:
  3. 65536 at com. Android. Dx. Merge. DexMerger$6. UpdateIndex (DexMerger. Java: 502) at com. Android. Dx. Me rgeDexMerger$ I dMe rgerme rgeSor ted (DexMerge r. Java:283)
  4. at com. Android. Dx. Merge. DexMerger. MergeMethodIds (DexMerger. Java: 491) at com. Android. Dx. Merge. DexMer ger. Me rgeDexes (DexMerger. Java:168) at com. Android. Dx. MergeDexMe rgermerge (DexMerger. Java: 189)
  5. at com. Android. Dx. Commanddexer. Main. MergeLibraryDexBuf fers (Main.java:454)at com. Android. Dx. Command. Dexer. Main. RunMonoDex (Main. Java: 303)
  6. at com. Android. Dx. Command. Dexer. Main. Run (Main. Java:246)
  7. at com. Android. Dx. Command. Dexer. Main. Main (Ma in. Java:215)
  8. at com. Android. Dxcommand. Main. Main (Main. Java:106)

如何解决方法数越界的问题呢?首先是删除无用的代码和第三方库。但是很多情况下即使则除无用的代码,方法数仍然越界,这个时候该怎么办呢?针对这个问题,之前很多应用都会考虑采用插件化的机制来动态加载部分dex,通过将一个dex拆分成两个或多个dex。这就在一定程度上解决了方法数的越界的问题,但是插件化是套重最级的技术方案。并且其兼客性问题往往较多,方法数越界的角度来说,插件化并不是一个非常适合的方案,为了解决这个问题,Coogle在2014年提出了Multidex解决方案,通过multidex可以很好地解决方法数越界的问题,并且使用起来非常简单。

基本使用

在Android 5.0以前使用multidex需要引入Google提供的android-support-multidex.jar这个jar包,这个jar包可以在Android SDK目录下的extras/android/support/multidex/library/libs下面找到。从Android 5.0开始,Android默认支持了multidex,它可以从apk中加载多个dex文件。通过查看Multidex.intsall的源码就可以发现,如果是5.0及其以上的系统,Multidex.install方法直接返回,并不会做任何操作。

在 AndroidStudio和Gradle编译环境中,如果要使用 multidex,首先要使用Android SDK Build Tools 21.1 及以上版本,接着修改工程中app目录下的 build. Gradle文件,在defaultConfig 中添加multiDexEnabled true这个配置项,如下所示。

  1. android {
  2. ....
  3. defaultConfig {
  4. applicationId "tk.thinkerzhangyan.multidexdemo"
  5. minSdkVersion 20
  6. targetSdkVersion 26
  7. versionCode 1
  8. versionName "1.0"
  9. testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  10. multiDexEnabled true
  11. }
  12. ....
  13. }

接着还需要在 dependencies 中添加 multidex 的依赖:compile 'com.android.support:multidex:1.0.0',如下所示。

  1. dependencies {
  2. ....
  3. compile 'com.android.support:multidex:1.0.0'
  4. ....
  5. }

最终配置完成的build.gradle文件如下所示,其中加粗的部分是专为multidex所添加的配置项:

  1. apply plugin: 'com.android.application'
  2. android {
  3. compileSdkVersion 26
  4. buildToolsVersion "26.0.0"
  5. defaultConfig {
  6. applicationId "tk.thinkerzhangyan.multidexdemo"
  7. minSdkVersion 20
  8. targetSdkVersion 26
  9. versionCode 1
  10. versionName "1.0"
  11. testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
  12. multiDexEnabled true
  13. }
  14. buildTypes {
  15. release {
  16. minifyEnabled false
  17. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  18. }
  19. }
  20. }
  21. dependencies {
  22. compile fileTree(dir: 'libs', include: ['*.jar'])
  23. androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
  24. exclude group: 'com.android.support', module: 'support-annotations'
  25. })
  26. compile 'com.android.support:multidex:1.0.0'
  27. compile 'com.android.support:appcompat-v7:26.+'
  28. compile 'com.android.support.constraint:constraint-layout:1.0.2'
  29. testCompile 'junit:junit:4.12'
  30. }

经过了上面的过程,还需要做另一项工作,那就是在代码中加入支持multidex的功能,这个过程是比较简单的,有三种方案可以选。

第一种方案,在manifest文件中指定Application为MultiDexApplication,如下所示。

  1. <application
  2. android: name="android.support.multidex.MultiDexApplication”
  3. android: allowBackup ="true
  4. android: icon="@mi pmap/ic_launcher"
  5. android: label="@string/app_name”
  6. android: theme="@style/AppTheme>
  7. </application>

第二种方案,让应用Application继承MultiDexApplication,比如:

  1. public class TestApplication extends MultiDexApplication{
  2. }

第三种方案,如果不想让应用的Application继承 MultiDexApplication,还可以选择重写 Application 的attachBaseContext方法,这个方法比Application的onCreate要先执行,如下所示。

  1. public class MyApplication extends Application {
  2. @Override
  3. protected void attachBaseContext(Context base) {
  4. super.attachBaseContext(base);
  5. MultiDex.install(this);
  6. }
  7. }

现在所有的工作都已经完成了,可以发现应用不但可以编译通过了并且还可以在Android 2.X 手机上面正常安装了。可以发现,multidex使用起来还是很简单的,对于一个使用multidex 方案的应用,采用了上面的配置项,如果这个应用的方法数没有越界,那么Gradle并不会生成多个 dex文件,如果方法数越界后,Gradle就会在apk中打包2个或多个dex文件,具体会打包多少个dex 文件要看当前项目的代码规模。

Android虚拟机相关知识

接下来我们会介绍使用Multidex的过程中可能会遇到的问题,如果想要了解这些问题出现的原因,我们需要了解一些Android虚拟机方面的知识,所以特地在此单独列出了一个小节。

关于Android虚拟机,建议阅读:Android虚拟机的一些知识&Android热修复基础知识

特地强调下:4.4之前DVM,在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。

MultiDex存在的问题

MultiDex机制的出现本身是为了避免出现app 65535问题的出现,但随着业务逻辑的增长,以及不合理的模块划分,可能会导致main dex的方法数也超出了65535,这就导致了main dex capacity exceeded异常

此外,Multidex的接入还会对app的启动性能造成影响。Multidex在install时需要加载dex,首次启动时还需要做odex的转换(只是首次启动的时候会进行dexopt操作,之后启动的时候会使用缓存起来的odex文件,所以不用再进行dexopt操作),而这些都是在ui主线程中完成。dexopt操作是比较耗时的,因此这可能导致第一启动的时候,出现黑屏,甚至ANR异常

根据 Carlos Sessa的测试,启用multidex后,4.4或以下的设备,app的启动时间平均会增加15%,更严重的情况,甚至在启动时候会出现了黑屏。

由于5.0以上的Android系统采用了ART运行时,它本身就支持multidex的加载,所以5.0以上系统影响较小。但是5.0以下的系统将会在加载主dex之外的类时会有比较明显的延迟。

此外,使用Multidex的时候,还可能会在Second Dex未加载的情况下使用在Second Dex中的类,从而导致Crash的发生。

总结来说,Multidex会导致下列问题:

因此目前部分app采取的策略是,放弃掉Multidex的,而转为插件化的架构。通过将非核心模块的lazy load,来达到启动速度的优化,但我们需要明确的是,并不是所有app都适合插件化架构,为了实现启动加速将本耦合的业务逻辑硬生生拆解其实是本末倒置。关于如何解决这些问题,请阅读问题解决方案章节。

Multidex在DVM和ART上的不同表现

需要注意的是,前面提到的使用Multidex的过程中遇到的问题,仅仅会出现在运行DVM的Android系统上,而对运行ART虚拟机的Android系统,则不会出现这个问题。原因如下:

DVM如果采用了分包技术的话,在安装阶段,会对miandex执行dexopt操作,在应用第一次启动的时候,会对second dex进行dexopt操作,而dexopt操作是比较耗时的,所以会导致黑屏设置ANR异常出现,同时也可能会出现由于类找不到而导致的崩溃。

Art VM在安装阶段就会合并所有的dex,dexoat整体只触发一次,而且是在应用的安装阶段。所以在使用Multidex的时候,不会出现前面提到的问题。

ART是内建支持MultiDex的,在应用的安装阶段就已经执行了dex的合并和加载操作了。通过查看Multidex.install()方法的源码我们可以发现,如果检测到当前系统的虚拟机是ART虚拟机,就直接返回了,也就是不会进行耗时的操作,所以在ART虚拟机上面,使用Multidex,不会出现黑屏现象。

  1. public static void install(Context context) {
  2. if (IS_VM_MULTIDEX_CAPABLE) {
  3. Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
  4. return;
  5. }
  6. //...
  7. }

通过前面的分析,我们可以知道,如果应用运行在ART虚拟机上,我们不在代码中添加Multidex.install()操作也是可以的,因为系统帮助我们进行了Second dex的加载操作。

ART虚拟机是如何内建支持MultiDex的?

Android ART、Dalvik在multidex上的差异、关联

Multidex导致应用首次启动出现黑屏(严重出现ANR)和崩溃的原因

对于DV

  1. 在app第一次安装到手机之后,系统运行dexopt程序对dex进行优化,只处理mainclass.dex,将dex的依赖库文件和一些辅助数据打包成odex文件
  2. 存放在cache/dalvik_cache目录下,保存格式为apk路径 @ apk名 @ classes.dex。
  3. dalvik vm运行期会把未加载的Second dex等调用dexopt进行再次转化和存储(MultiDex.install)。

因为应用第一次启动的时候,会在主线程对Second dex进行dexopt操作,而这个操作是非常耗时的,所以可能会导致应用启动速度过慢,导致黑屏现象出现,甚至导致ANR。

应用第一次启动的时候,如果在Multidex.install之前,引用到了Second dex中的代码,会找不到相应的代码(因为还没有被加载),从而出现崩溃。

对于ART虚拟机

Art VM在安装阶段就会合并所有的dex,dexopt整体只触发一次。所以在应用启动的时候,不会出现黑屏异常和由于类找不到而触发的崩溃。

总结

Multidex导致的启动过慢问题和崩溃问题,仅仅会出现在DV上,而不会出现在ART虚拟机上。

关于Android虚拟机的知识,建议阅读前面的Android虚拟机相关知识小节。

关于Android 64K引发的MultiDex你想知道的都在这里:一场由启动黑屏引发的惨案-5.1

问题解决方案

第一次启动速度变慢和黑屏问题(严重ANR)解决方案

整体思路

  • Multidex异步化

通过前面的原因分析我们知道,之所以会出现启动过慢,黑屏,ANR问题,是因为我们在主线程中进行了耗时的Multidex.install操作。所以我们可以选择使用异步的方式来进行Multidex.install操作。

在Android的性能优化中,最常见的思路就是异步化,减少UI线程的工作。在应用的交互层面上,app启动时,几乎所有app都会有一个SplashActivity。在界面上展示欢迎页,在后台进行初始化的业务逻辑。这就给我们一个启发,我们可以将系统的初始化逻辑,延迟到我们的业务初始化时间点上。

但是这里需要注意的是,不能使用开新线程进行加载,而主线程继续执行Application初始化的方案。这是因为子线程multidex安装没有结束,dex还没加载进来(意味着部分类还没有有加载进来),这时候由于主线程继续执行,应用使用到了seconday.dex里的类(未加载的类),会导致崩溃出现。而如果我们选择主线程等待的话,又会出现我们想要解决的问题。。。。

不能使用线程来完成异步操作,但是我们可以利用多进程来完成异步操作

具体思路

  1. 在Application.attachBaseContext(Context base)中,判断是否初次启动,以及系统版本是否小于5.0,如果是,跳到2;否则,直接执行MultiDex.install(Context context)。
  2. 开启一个新进程,在这个进程中执行MultiDex.install(Context context)。执行完毕,唤醒主进程,自身结束。主进程在开启新进程后,自身是挂起的,直到被唤醒。
  3. 唤醒的主进程继续执行初始化操作。

流程图:

此处输入图片的描述

可能你会有疑问,挂住主进程过程中,是否会产生ANR?事实上是不会的,因为我们拉起Loaddex进程后,主进程已经不是前台进程了,经过测试在attachBaseContext,无论将要启动的Activity、Broadcast还是Service,尽管卡住100s,也不会出现ANR(回想ANR的几个原因,按键消息、Broadcast onReceiver或者Service)。

具体实现一

  1. import android.app.ActivityManager;
  2. import android.content.ComponentName;
  3. import android.content.Context;
  4. import android.content.Intent;
  5. import android.content.SharedPreferences;
  6. import android.content.pm.ApplicationInfo;
  7. import android.content.pm.PackageInfo;
  8. import android.content.pm.PackageManager;
  9. import android.os.Build;
  10. import android.support.multidex.MultiDex;
  11. import com.xx.xx.common.BaseFunctionConfig;
  12. import com.xx.xx.log.LogCore;
  13. import com.xx.xx.login.activity.LoadResActivity;
  14. import com.xx.xx.utils.StringUtils;
  15. import java.util.Map;
  16. import java.util.jar.Attributes;
  17. import java.util.jar.JarFile;
  18. /**
  19. * 类描述:
  20. * <p>
  21. * Created by yhf on 2018/1/19.
  22. */
  23. public class BaseMultiDexApplication extends BaseMoaApplication {
  24. public static final String TAG = "BaseMultiDexApplication";
  25. public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
  26. @Override
  27. protected void attachBaseContext(Context base) {
  28. super .attachBaseContext(base);
  29. LogCore.i( TAG, "App attachBaseContext ");
  30. //是<5.0的系统 && 是主进程(不是loaddex进程) 则进入异步加载方案
  31. if (!isLoadDexProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
  32. if (needWait(base)){//判断dexopt未执行过
  33. waitForDexopt(base);
  34. }
  35. //主进程加载dex,
  36. //此时odex已经产生(dexopt操作已经在 loaddexActivity中执行过了,或者不是第一次打开应用),所以不会有耗时问题
  37. MultiDex.install (this );
  38. } else {
  39. //>=5.0的系统默认对dex进行oat优化,不需要MultiDex.install (this );
  40. return;
  41. }
  42. }
  43. @Override
  44. public void onCreate() {
  45. super .onCreate();
  46. if (isLoadDexProcess()) {
  47. return;
  48. }
  49. }
  50. /*****************************判断是否是 loaddex进程********************************/
  51. public boolean isLoadDexProcess() {
  52. if (StringUtils.containsIgnoreCase( getCurProcessName(this), ":mini")) {
  53. LogCore.i( TAG, ":mini start!");
  54. return true;
  55. }
  56. return false ;
  57. }
  58. /*****************************判断dexopt是否已经执行过********************************/
  59. /**
  60. * 判断dexopt是否已经执行过
  61. * 通过校验本地存储的md5记录和apk里的classes2.dex是否一致
  62. * neead wait for dexopt ?
  63. */
  64. private boolean needWait(Context context){
  65. String flag = get2thDexSHA1(context);
  66. LogCore.i( TAG, "dex2-sha1 "+flag);
  67. SharedPreferences sp = context.getSharedPreferences(
  68. getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
  69. String saveValue = sp.getString(KEY_DEX2_SHA1, "");
  70. return !StringUtils.equals(flag,saveValue);
  71. }
  72. //Get classes.dex file signature
  73. private String get2thDexSHA1(Context context) {
  74. ApplicationInfo ai = context.getApplicationInfo();
  75. String source = ai.sourceDir;
  76. try {
  77. JarFile jar = new JarFile(source);
  78. java.util.jar.Manifest mf = jar.getManifest();
  79. Map<String, Attributes> map = mf.getEntries();
  80. Attributes a = map.get("classes2.dex");
  81. return a.getValue("SHA1-Digest");
  82. } catch (Exception e) {
  83. e.printStackTrace();
  84. }
  85. return null ;
  86. }
  87. /*****************************阻塞等待********************************/
  88. /**
  89. * 1. 启动 异步dexopt的跨进程Activity
  90. * 2. 阻塞当前主进程
  91. * 3. 200ms间隔轮训dexopt是否完成,超时或者已完成,则唤醒主进程
  92. * 3.1 在attachContext中的MultixDex.install,如果超时,则主进程自己再次同步执行dexopt,相当于恢复到官方默认方案;
  93. * 3.2 在attachContext中的MultixDex.install,如果已完成,则主进程不再执行的dexopt,单纯加载odex,提升速度
  94. */
  95. public void waitForDexopt(Context base) {
  96. Intent intent = new Intent();
  97. ComponentName componentName = new
  98. ComponentName(BaseFunctionConfig.PACKAGE_NAME, LoadResActivity.class.getName());
  99. intent.setComponent(componentName);
  100. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  101. base.startActivity(intent);
  102. long startWait = System.currentTimeMillis ();
  103. long waitTime = 10 * 1000 ;
  104. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) {
  105. waitTime = 20 * 1000 ;//实测发现某些场景下有些2.3版本有可能10s都不能完成optdex
  106. }
  107. while (needWait(base)) {
  108. //application启动了LoadDexActivity之后,自身不再是前台进程所以怎么hold 线程都不会ANR
  109. try {
  110. long nowWait = System.currentTimeMillis() - startWait;
  111. LogCore.i( TAG, "wait ms :" + nowWait);
  112. if (nowWait >= waitTime) {
  113. return;
  114. }
  115. Thread.sleep(200 );
  116. } catch (InterruptedException e) {
  117. e.printStackTrace();
  118. }
  119. }
  120. }
  121. /*****************************utils********************************/
  122. //LoadResActivity 中被调用
  123. public void installFinish(Context context){
  124. SharedPreferences sp = context.getSharedPreferences(
  125. getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
  126. sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
  127. }
  128. public static PackageInfo getPackageInfo(Context context){
  129. PackageManager pm = context.getPackageManager();
  130. try {
  131. return pm.getPackageInfo(context.getPackageName(), 0);
  132. } catch (PackageManager.NameNotFoundException e) {
  133. LogCore.i(TAG, e.getLocalizedMessage());
  134. }
  135. return new PackageInfo();
  136. }
  137. public static String getCurProcessName(Context context) {
  138. try {
  139. int pid = android.os.Process.myPid();
  140. ActivityManager mActivityManager = (ActivityManager) context
  141. .getSystemService(Context. ACTIVITY_SERVICE);
  142. for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
  143. .getRunningAppProcesses()) {
  144. if (appProcess.pid == pid) {
  145. return appProcess. processName;
  146. }
  147. }
  148. } catch (Exception e) {
  149. // ignore
  150. }
  151. return null ;
  152. }
  153. }
  • Launcher Activity 依然是原来的代码里的WelcomeActivity
  • 在Application启动的时候会检测dexopt是否已经完成过,(检测方式是查看sp文件是否有dex文件的SHA1-Digest记录,这里要两个进程读取该sp,读取模式是MODE_MULTI_PROCESS)
    • 如果没有就启动LoadDexActivity(属于:mini进程) 。
    • 否则就直接install dex !通过日志发现,已经dexopt的dex文件再次install的时候 只耗费几十毫秒
  • LoadDexActivity 的逻辑比较简单,启动AsyncTask 来install dex 这时候会触发dexopt
  1. public class LoadResActivity extends Activity {
  2. @Override
  3. public void onCreate(Bundle savedInstanceState) {
  4. requestWindowFeature(Window.FEATURE_NO_TITLE);
  5. super .onCreate(savedInstanceState);
  6. getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN );
  7. overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
  8. setContentView(R.layout.activity_verify);
  9. new LoadDexTask().execute();
  10. }
  11. class LoadDexTask extends AsyncTask {
  12. @Override
  13. protected Object doInBackground(Object[] params) {
  14. try {
  15. MultiDex.install(getApplication());
  16. LogUtils.d("loadDex" , "install finish" );
  17. ((App) getApplication()).installFinish(getApplication());
  18. } catch (Exception e) {
  19. LogUtils.e("loadDex" , e.getLocalizedMessage());
  20. }
  21. return null;
  22. }
  23. @Override
  24. protected void onPostExecute(Object o) {
  25. LogUtils.d( "loadDex", "get install finish");
  26. finish();
  27. System.exit( 0);
  28. }
  29. }
  30. @Override
  31. public void onBackPressed() {
  32. //cannot backpress
  33. }

Manifest.xml 里面

  1. <activity
  2. android:name= "com.zongwu.LoadResActivity"
  3. android:launchMode= "singleTask"
  4. android:process= ":mini"
  5. android:alwaysRetainTaskState= "false"
  6. android:excludeFromRecents= "true"
  7. android:screenOrientation= "portrait" />
  8. <activity
  9. android:name= "com.zongwu.WelcomeActivity"
  10. android:launchMode= "singleTop"
  11. android:screenOrientation= "portrait">
  12. <intent-filter >
  13. <action android:name="android.intent.action.MAIN"/>
  14. <category android:name="android.intent.category.LAUNCHER"/>
  15. </intent-filter >
  16. </activity>

替换Activity默认的出现动画 R.anim.null_anim 文件的定义

  1. -<set xmlns:android="http://schemas.android.com/apk/res/android">
  2. <alpha
  3. android:fromAlpha="1.0"
  4. android:toAlpha="1.0"
  5. android:duration="550"/>
  6. </set>

Application具有继承结构的注意,在子类中判断

  1. if (isLoadDexProcess()) {
  2. return;
  3. }

因为有大量代码在MultiDex.install之前执行,因此必须把涉及到的class以显示声明的方式声明放到主dex中

  1. //app/build.gradle
  2. android {
  3. //...
  4. defaultConfig {
  5. //...
  6. //定义main dex中必须保留的类
  7. multiDexKeepProguard file('mainDexClasses.pro')
  8. }
  9. }
  10. //app/mainDexClasses.pro
  11. -keep class android.content.Intent { *; }
  12. -keep interface android.content.SharedPreferences { *; }
  13. //...自己看引用
存在的问题

这种方式好处在于依赖集非常简单,同时它的集成方式也是非常简单,我们无须去修改与加载无关的代码。

  1. 对现有代码改动量最小。
  2. 该方案不关注Application被哪个组件启动。Activity ,Service ,Receiver ,ContentProvider 都满足。(有个问题要说明:如细心网友指出的那样,新安装还未启动但是收到Receiver的场景下,会导致Load界面出现。这个场景实际出现几率比较少,且仅出现一次。可以接受。)
  3. 该方案不限制 Application ,Activity ,Service ,Receiver ,ContentProvider 继续新增业务。

但是:使用“使用MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件”来判断是否dexopt完成的手段,在6.0上因为google废除了该标记导致行为准确性的难以保证

具体实现二

思路

使用“使用MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件”来判断是否dexopt完成的手段,在6.0上因为google废除了该标记导致行为准确性的难以保证

针对该环节进行优化,通过 跨进程通讯的Messenger来实现

BaseApplication

  1. public abstract class BaseApplication extends Application {
  2. @SuppressWarnings("MismatchedReadAndWriteOfArray")
  3. private static final byte[] lock = new byte[0];
  4. @Override
  5. protected void attachBaseContext(Context base) {
  6. super.attachBaseContext(base);
  7. //是<5.0的系统 && 是主进程(不是loaddex进程) 则进入异步加载方案
  8. if (!isLoadDexProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
  9. //判断dexopt未执行过
  10. if (needWait(base)) {
  11. DexInstallDeamonThread thread = new DexInstallDeamonThread(this, base);
  12. thread.start();
  13. //阻塞等待:async_launch完成加载
  14. synchronized (lock) {
  15. try {
  16. lock.wait();
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. thread.exit();
  22. Log.d("BaseApplication", "dexopt finished. alloc MultiDex.install()");
  23. } else {
  24. //主进程加载dex,
  25. //此时odex已经产生(dexopt操作已经在 loaddexActivity中执行过了,或者不是第一次打开应用),所以不会有耗时问题
  26. MultiDex.install(this);
  27. }
  28. }else{
  29. //>=5.0的系统默认对dex进行oat优化,不需要MultiDex.install (this );
  30. return;
  31. }
  32. }
  33. /*****************************判断是否是 loaddex进程********************************/
  34. public boolean isLoadDexProcess() {
  35. String processName = getCurProcessName(this);
  36. return processName != null && processName.contains(":mini");
  37. }
  38. /****************************判断dexopt是否已经执行过********************************/
  39. /**
  40. * 另一种手段判断是否dexopt,将其换转为 判断是否是首次启动
  41. * 这个标记应当随着版本升级而重置
  42. * @param context
  43. * @return
  44. */
  45. public final static String IS_FIRST_LAUNCH = "";
  46. @SuppressWarnings("deprecation")
  47. private boolean needWait(Context context) {
  48. //这里实现不唯一,读取一个全局的标记,判断是否初次启动APP
  49. SharedPreferences sp = context.getSharedPreferences(
  50. getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
  51. return sp.getBoolean(IS_FIRST_LAUNCH, true);
  52. }
  53. /*****************************阻塞等待********************************/
  54. /**
  55. * 基于Messenger的跨进程通讯方式
  56. * 1. 启动 异步dexopt 的跨进程Activity
  57. * 2. 阻塞当前主进程
  58. * 3. 锁机制等待dexopt是否完成
  59. */
  60. private static class DexInstallDeamonThread extends Thread {
  61. private Handler handler;
  62. private Context application;
  63. private Context base;
  64. private Looper looper;
  65. public DexInstallDeamonThread(Context application, Context base) {
  66. this.application = application;
  67. this.base = base;
  68. }
  69. @SuppressLint("HandlerLeak")
  70. @Override
  71. public void run() {
  72. Looper.prepare();
  73. looper = Looper.myLooper();
  74. handler = new Handler() {
  75. @SuppressWarnings("deprecation")
  76. @Override
  77. public void handleMessage(Message msg) {
  78. synchronized (lock) {
  79. lock.notify();
  80. }
  81. SPUtils
  82. .getVersionSharedPreferences(application)
  83. .edit()
  84. .putBoolean(IS_FIRST_LAUNCH, false)
  85. .apply();
  86. }
  87. };
  88. Messenger messenger = new Messenger(handler);
  89. Intent intent = new Intent(base, LoadResActivity.class);
  90. intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  91. intent.putExtra("MESSENGER", messenger);
  92. base.startActivity(intent);
  93. Looper.loop();
  94. }
  95. public void exit() {
  96. if (looper != null) looper.quit();
  97. }
  98. }
  99. /*****************************utils********************************/
  100. public static String getCurProcessName(Context context) {
  101. try {
  102. int pid = android.os.Process.myPid();
  103. ActivityManager mActivityManager = (ActivityManager) context
  104. .getSystemService(Context. ACTIVITY_SERVICE);
  105. for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
  106. .getRunningAppProcesses()) {
  107. if (appProcess.pid == pid) {
  108. return appProcess. processName;
  109. }
  110. }
  111. } catch (Exception e) {
  112. // ignore
  113. }
  114. return null ;
  115. }
  116. public static PackageInfo getPackageInfo(Context context){
  117. PackageManager pm = context.getPackageManager();
  118. try {
  119. return pm.getPackageInfo(context.getPackageName(), 0);
  120. } catch (PackageManager.NameNotFoundException e) {
  121. Log.i("getPackaigeInfo", e.getLocalizedMessage());
  122. }
  123. return new PackageInfo();
  124. }
  125. }

LoadResActivity

  1. public class LoadResActivity extends AppCompatActivity {
  2. private Messenger messenger;
  3. @Override
  4. public void onCreate(Bundle savedInstanceState) {
  5. requestWindowFeature(Window.FEATURE_NO_TITLE);
  6. super.onCreate(savedInstanceState);
  7. getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
  8. overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
  9. setContentView(R.layout.activity_load_res);
  10. Log.d("LoadResActivity", "start install");
  11. Intent from = getIntent();
  12. messenger = from.getParcelableExtra("MESSENGER");
  13. LoadDexTask dexTask = new LoadDexTask();
  14. dexTask.execute();
  15. }
  16. class LoadDexTask extends AsyncTask<Void, Void, Void> {
  17. @Override
  18. protected Void doInBackground(Void... params) {
  19. try {
  20. MultiDex.install(getApplication());
  21. Log.d("LoadResActivity", "finish install");
  22. messenger.send(new Message());
  23. } catch (Exception e) {
  24. e.printStackTrace();
  25. }
  26. return null;
  27. }
  28. @Override
  29. protected void onPostExecute(Void o) {
  30. finish();
  31. System.exit(0);
  32. }
  33. }
  34. @Override
  35. public void onBackPressed() {
  36. //无法退出
  37. }
  38. }

app/AndroidManifest.xml加入:

  1. <activity
  2. android:name="com.synaric.dex.LoadResActivity"
  3. android:launchMode= "singleTask"
  4. android:alwaysRetainTaskState= "false"
  5. android:excludeFromRecents= "true"
  6. android:screenOrientation= "portrait"
  7. android:process=":async_launch"/>

因为有大量代码在MultiDex.install之前执行,因此必须把涉及到的class以显示声明的方式声明放到主dex中

  1. //app/build.gradle
  2. android {
  3. //...
  4. defaultConfig {
  5. //...
  6. //定义main dex中必须保留的类
  7. multiDexKeepProguard file('mainDexClasses.pro')
  8. }
  9. }
  10. //app/mainDexClasses.pro
  11. -keep public class * extends java.lang.Thread { *; }
  12. -keep interface android.content.SharedPreferences { *; }
  13. -keep class android.os.Handler { *; }
  14. -keep class com.synaric.common.BaseSPKey { *; }
  15. -keep class android.os.Messenger { *; }
  16. -keep class android.content.Intent { *; }
  17. //...自己看引用

该小节参考链接:

关于Android 64K引发的MultiDex你想知道的都在这里:一场由启动黑屏引发的惨案(非常详细,主要参考了这篇文章)

Android MultiDex初次启动APP优化

其实你不知道MultiDex到底有多坑

应用崩溃(Crash)解决方案

由于Multidex导致的Crash,adb logcat看下,基本也就是3类问题:

  • NoClassDefFoundError

  • ClassNotFoundException

  • Could not find method

所有这些都是同一个问题导致的:classes2.dex没加载完成之前,程序调用了classes2.dex中的类或者方法!

解决方案:将这些类,分包到MainDex里面。

怎么解决java.lang.NoClassDefFoundError错误

将某些类分包到Maindex中(待验证)

当你在项目中使用了multidex的时候,你的app可能会产生上述的异常。这意味着你的app在启动的时候没有找到含有指定类的class文件。

Android的Gradle插件首先需要SDK build tools 21.1及以上才支持multidex,它会在混淆工程之后列出一个主dex文件中包含的类的清单([buildDir]/intermediates/multi-dex/[buildType]/maindexlist.txt)。但这里面可能没有包含所有在App启动时需要加载的类,这时启动App就会抛出异常。

要解决这个问题,你要列出一份启动App时需要加载的类的清单,并告诉编译器这些类要保留在主dex文件中。你可以这么做:

方案一
  1. android.applicationVariants.all { variant ->
  2. task "fix${variant.name.capitalize()}MainDexClassList" << {
  3. logger.info "Fixing main dex keep file for $variant.name"
  4. File keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")
  5. keepFile.withWriterAppend { w ->
  6. // Get a reader for the input file
  7. w.append('\n')
  8. new File("${projectDir}/multidex.keep").withReader { r ->
  9. // And write data from the input into the output
  10. w << r << '\n'
  11. }
  12. logger.info "Updated main dex keep file for ${keepFile.getAbsolutePath()}\n$keepFile.text"
  13. }
  14. }
  15. }
  16. tasks.whenTaskAdded { task ->
  17. android.applicationVariants.all { variant ->
  18. if (task.name == "create${variant.name.capitalize()}MainDexClassList") {
  19. task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"
  20. }
  21. }
  22. }

上述代码,参考自Android Multidex导致的App启动缓慢,乐视视频项目中就是用的这个方案,这篇文章中还介绍了如何检测,second dex中的那些类,在应用启动的时候会被加载的方法,建议稍后阅读

MainDexList产生过程源码分析(建议读读)

Android 项目开发填坑记 - 使用 MultiDex 解决 64K 限制(似乎讲错了)

关于Android 64K引发的MultiDex你想知道的都在这里:一场由启动黑屏引发的惨案(。。。。又对又错)

异步加载 multidex(感觉作者好像都没有搞清楚什么事情)

MultiDex 编译过程(相当详细的讲解了Multidex的工作原理)

Android MultiDex 实践:如何绕过那些坑?(MultiDex编译过程,讲解了分包的几个Task)

谈谈MultiDex启动优化(这里面也详细的讲解了如何分包的)

方案二

我们需要通过修改build.gradle文件,增加afterEvaluate区域。下面给出完整的build.gradle配置,其中1,3两项配置在dex分包方案概述与multidex包的配置使用中已经介绍过,配置如下:

  1. apply plugin: 'com.android.application'
  2. android {
  3. compileSdkVersion 23
  4. buildToolsVersion "22.0.1"
  5. defaultConfig {
  6. applicationId "com.example.gao.delete"
  7. minSdkVersion 17
  8. targetSdkVersion 23
  9. versionCode 1
  10. versionName "1.0"
  11. //1
  12. multiDexEnabled true
  13. }
  14. buildTypes {
  15. release {
  16. minifyEnabled false
  17. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  18. }
  19. }
  20. }
  21. //2
  22. afterEvaluate {
  23. tasks.matching {
  24. it.name.startsWith('dex')
  25. }.each { dx ->
  26. def listFile = project.rootDir.absolutePath+'/app/maindexlist.txt'
  27. if (dx.additionalParameters == null) {
  28. dx.additionalParameters = []
  29. }
  30. //表示当方法数越界时则生成多个dex文件(我的没有越界,貌似也生成了两个)
  31. dx.additionalParameters += '--multi-dex'
  32. //这个指定了listFile中的类(即maindexlist.txt中的类)会打包到主dex中,不过注意下一条。
  33. dx.additionalParameters += '--main-dex-list=' +listFile
  34. //表明只有-main-dex-list所指定的类(在我的配置中,就是app目录下的maindexlist.txt中包含的类)才能打包到主dex中,如果没有这个选项,上个选项就会失效
  35. dx.additionalParameters += '--minimal-main-dex'
  36. }
  37. }
  38. dependencies {
  39. compile fileTree(dir: 'libs', include: ['*.jar'])
  40. compile 'com.android.support:appcompat-v7:24.0.0-alpha1'
  41. //3
  42. compile 'com.android.support:multidex:1.0.0'
  43. }

根据上面builde.gradle中的配置,我们在app目录下创建一个maindexlist.txt,我们在这个txt里将我们想要放在主dex中的类写进去,自己写还是相当麻烦的,如果自己不会脚本自动生成的话,可以在\app\build\intermediates\multi-dex\debug目录下找到了一个maindexlist.txt,注意,这个你改了没用,一运行又恢复了,将这个复制到app目录下,然后将我们想要打包到MainDex中的类,按照要求的格式,添加进去即可。

multidex分包续:将指定的类打包到主dex中

《Android开发艺术探索》

方案三

在gradle.build的defaultConfig中可以指定一个文件来表示要放在第一个dex中的class

加入

  1. multiDexKeepFile file('maindexlist.txt')

然后在gradle.build的同级目录下新建一个文件maindexlist.txt

里面填上要放在第一个dex中的class

如:

  1. com/android/reverse/mod/ReverseXposedModule.class
  2. top/imlk/xpmodulemultidexer/XMMultiDex.class

也可以用

  1. multiDexKeepProguard file('maindexlist.pro')

然后
新建maindexlist.pro文件,这样就可以用proguard语法来写

  1. -keep class com.android.reverse.mod.ReverseXposedModule
  2. -keep class top.imlk.xpmodulemultidexer.*

可算是ok了。

Android实现Multidex及指定主dex中的class

方案四

第一次启动速度变慢和黑屏问题(严重ANR)解决方案里面的分包方案。

主Dex方法数超65536的解决方案

精简maindex,对于启动不需要的代码,将其分包到Second dex中。

谈谈MultiDex启动优化(这里面也详细的讲解了如何分包的)

MultiDex源码相关

关于MultiDex的源码分析,建议阅读:MultiDex源码分析

面试问题总结

Android 上为啥会有65536的限制,解释下原因。

简单回答:Android应用中的方法,最终是通过虚拟机指令进行调用执行的,虚拟机中执行方法调用指令的invoke-kind, invoke-kind(调用各类方法)指令中,方法引用索引数是16位的,也就是最多调用 2^16 = 65536 个方法,所以Dex中的方法超过65536之后会出错。

建议阅读:Android 上为啥会有65536的限制,解释下原因

MultiDex 在打包阶段和 app 运行阶段分别做了啥事吗?

我们经常说的MultiDex,可以分成运行时和编译时两个部分:

编译时的分包机制,将app中的class以某种策略分散在多个dex中,目的是减少为了第一个dex也就是main dex中包含的class

运行时: app启动时,虚拟机只加载main dex中的class。app启动以后,使用Multidex.install API,通过反射修改ClassLoader中的dexElements加载其他dex

谈谈MultiDex启动优化

使用 MultiDex 可能会造成什么问题?

阅读本文前面章节。

使用 MultiDex 后首次启动 app 有什么优化方向吗?

阅读本文前面章节。

如何将指定的 class 打进 mainDex ?

阅读本文前面章节。

关于 65535 限制与 MultiDex 会在面试中被问到的问题可能都在这了

一些其他的参考链接:

《Android开发艺术探索》

Android MultiDex 实践:如何绕过那些坑?(MultiDex编译过程,讲解了分包的几个Task)

Android拆分与加载Dex的多种方案对比

Android的multidex带来的性能问题-减慢app启动速度(很烂,不建议看)

Android Multidex导致的App启动缓慢(详细的讲解了如何分包的)

异步加载 multidex

Android MultiDex;NoClassDefFoundError;5.0以下系统应用闪退

MultiDex 编译过程(相当详细的讲解了Multidex的工作原理)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注