@946898963
2020-07-15T17:50:56.000000Z
字数 24100
阅读 2589
Android热修复
在Android中单个dex文件所能够包含的最大方法数为65536,这包含Android FrameWork、依赖的jar包以及应用本身的代码中的所有方法。65536是一一个很大的数,一般来说一个简单应用的方法数的确很难达到65536,但是对于一-些比较大型的应用来说,65536就很容易达到了。当应用的方法数达到 65536 后,编译器就无法完成编译工作并抛出类似下面的异常:
UNEX PECTED TOP-LEVEL EXCEPT ION:
com. Android. Dex. DexIndexOverflowException: method ID not in [0,Oxffff]:
65536 at com. Android. Dx. Merge. DexMerger$6. UpdateIndex (DexMerger. Java: 502) at com. Android. Dx. Me rge。DexMerger$ I dMe rger。me rgeSor ted (DexMerge r. Java:283)
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. Merge。DexMe rger。merge (DexMerger. Java: 189)
at com. Android. Dx. Command。dexer. Main. MergeLibraryDexBuf fers (Main.java:454)at com. Android. Dx. Command. Dexer. Main. RunMonoDex (Main. Java: 303)
at com. Android. Dx. Command. Dexer. Main. Run (Main. Java:246)
at com. Android. Dx. Command. Dexer. Main. Main (Ma in. Java:215)
at com. Android. Dx。command. 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这个配置项,如下所示。
android {
....
defaultConfig {
applicationId "tk.thinkerzhangyan.multidexdemo"
minSdkVersion 20
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
....
}
接着还需要在 dependencies 中添加 multidex 的依赖:compile 'com.android.support:multidex:1.0.0',如下所示。
dependencies {
....
compile 'com.android.support:multidex:1.0.0'
....
}
最终配置完成的build.gradle文件如下所示,其中加粗的部分是专为multidex所添加的配置项:
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.0"
defaultConfig {
applicationId "tk.thinkerzhangyan.multidexdemo"
minSdkVersion 20
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:multidex:1.0.0'
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
经过了上面的过程,还需要做另一项工作,那就是在代码中加入支持multidex的功能,这个过程是比较简单的,有三种方案可以选。
第一种方案,在manifest文件中指定Application为MultiDexApplication,如下所示。
<application
android: name="android.support.multidex.MultiDexApplication”
android: allowBackup ="true”
android: icon="@mi pmap/ic_launcher"
android: label="@string/app_name”
android: theme="@style/AppTheme”>
</application>
第二种方案,让应用Application继承MultiDexApplication,比如:
public class TestApplication extends MultiDexApplication{
}
第三种方案,如果不想让应用的Application继承 MultiDexApplication,还可以选择重写 Application 的attachBaseContext方法,这个方法比Application的onCreate要先执行,如下所示。
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
}
现在所有的工作都已经完成了,可以发现应用不但可以编译通过了并且还可以在Android 2.X 手机上面正常安装了。可以发现,multidex使用起来还是很简单的,对于一个使用multidex 方案的应用,采用了上面的配置项,如果这个应用的方法数没有越界,那么Gradle并不会生成多个 dex文件,如果方法数越界后,Gradle就会在apk中打包2个或多个dex文件,具体会打包多少个dex 文件要看当前项目的代码规模。
接下来我们会介绍使用Multidex的过程中可能会遇到的问题,如果想要了解这些问题出现的原因,我们需要了解一些Android虚拟机方面的知识,所以特地在此单独列出了一个小节。
关于Android虚拟机,建议阅读:Android虚拟机的一些知识&Android热修复基础知识。
特地强调下:4.4之前DVM,在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。
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的Android系统上,而对运行ART虚拟机的Android系统,则不会出现这个问题。原因如下:
DVM如果采用了分包技术的话,在安装阶段,会对miandex执行dexopt操作,在应用第一次启动的时候,会对second dex进行dexopt操作,而dexopt操作是比较耗时的,所以会导致黑屏设置ANR异常出现,同时也可能会出现由于类找不到而导致的崩溃。
Art VM在安装阶段就会合并所有的dex,dexoat整体只触发一次,而且是在应用的安装阶段。所以在使用Multidex的时候,不会出现前面提到的问题。
ART是内建支持MultiDex的,在应用的安装阶段就已经执行了dex的合并和加载操作了。通过查看Multidex.install()方法的源码我们可以发现,如果检测到当前系统的虚拟机是ART虚拟机,就直接返回了,也就是不会进行耗时的操作,所以在ART虚拟机上面,使用Multidex,不会出现黑屏现象。
public static void install(Context context) {
if (IS_VM_MULTIDEX_CAPABLE) {
Log.i(TAG, "VM has multidex support, MultiDex support library is disabled.");
return;
}
//...
}
通过前面的分析,我们可以知道,如果应用运行在ART虚拟机上,我们不在代码中添加Multidex.install()操作也是可以的,因为系统帮助我们进行了Second dex的加载操作。
Android ART、Dalvik在multidex上的差异、关联
对于DV
因为应用第一次启动的时候,会在主线程对Second dex进行dexopt操作,而这个操作是非常耗时的,所以可能会导致应用启动速度过慢,导致黑屏现象出现,甚至导致ANR。
应用第一次启动的时候,如果在Multidex.install之前,引用到了Second dex中的代码,会找不到相应的代码(因为还没有被加载),从而出现崩溃。
对于ART虚拟机
Art VM在安装阶段就会合并所有的dex,dexopt整体只触发一次。所以在应用启动的时候,不会出现黑屏异常和由于类找不到而触发的崩溃。
总结
Multidex导致的启动过慢问题和崩溃问题,仅仅会出现在DV上,而不会出现在ART虚拟机上。
关于Android虚拟机的知识,建议阅读前面的Android虚拟机相关知识小节。
关于Android 64K引发的MultiDex你想知道的都在这里:一场由启动黑屏引发的惨案-5.1
- Multidex异步化
通过前面的原因分析我们知道,之所以会出现启动过慢,黑屏,ANR问题,是因为我们在主线程中进行了耗时的Multidex.install操作。所以我们可以选择使用异步的方式来进行Multidex.install操作。
在Android的性能优化中,最常见的思路就是异步化,减少UI线程的工作。在应用的交互层面上,app启动时,几乎所有app都会有一个SplashActivity。在界面上展示欢迎页,在后台进行初始化的业务逻辑。这就给我们一个启发,我们可以将系统的初始化逻辑,延迟到我们的业务初始化时间点上。
但是这里需要注意的是,不能使用开新线程进行加载,而主线程继续执行Application初始化的方案。这是因为子线程multidex安装没有结束,dex还没加载进来(意味着部分类还没有有加载进来),这时候由于主线程继续执行,应用使用到了seconday.dex里的类(未加载的类),会导致崩溃出现。而如果我们选择主线程等待的话,又会出现我们想要解决的问题。。。。
不能使用线程来完成异步操作,但是我们可以利用多进程来完成异步操作。
流程图:
可能你会有疑问,挂住主进程过程中,是否会产生ANR?事实上是不会的,因为我们拉起Loaddex进程后,主进程已经不是前台进程了,经过测试在attachBaseContext,无论将要启动的Activity、Broadcast还是Service,尽管卡住100s,也不会出现ANR(回想ANR的几个原因,按键消息、Broadcast onReceiver或者Service)。
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.multidex.MultiDex;
import com.xx.xx.common.BaseFunctionConfig;
import com.xx.xx.log.LogCore;
import com.xx.xx.login.activity.LoadResActivity;
import com.xx.xx.utils.StringUtils;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
/**
* 类描述:
* <p>
* Created by yhf on 2018/1/19.
*/
public class BaseMultiDexApplication extends BaseMoaApplication {
public static final String TAG = "BaseMultiDexApplication";
public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
@Override
protected void attachBaseContext(Context base) {
super .attachBaseContext(base);
LogCore.i( TAG, "App attachBaseContext ");
//是<5.0的系统 && 是主进程(不是loaddex进程) 则进入异步加载方案
if (!isLoadDexProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
if (needWait(base)){//判断dexopt未执行过
waitForDexopt(base);
}
//主进程加载dex,
//此时odex已经产生(dexopt操作已经在 loaddexActivity中执行过了,或者不是第一次打开应用),所以不会有耗时问题
MultiDex.install (this );
} else {
//>=5.0的系统默认对dex进行oat优化,不需要MultiDex.install (this );
return;
}
}
@Override
public void onCreate() {
super .onCreate();
if (isLoadDexProcess()) {
return;
}
}
/*****************************判断是否是 loaddex进程********************************/
public boolean isLoadDexProcess() {
if (StringUtils.containsIgnoreCase( getCurProcessName(this), ":mini")) {
LogCore.i( TAG, ":mini start!");
return true;
}
return false ;
}
/*****************************判断dexopt是否已经执行过********************************/
/**
* 判断dexopt是否已经执行过
* 通过校验本地存储的md5记录和apk里的classes2.dex是否一致
* neead wait for dexopt ?
*/
private boolean needWait(Context context){
String flag = get2thDexSHA1(context);
LogCore.i( TAG, "dex2-sha1 "+flag);
SharedPreferences sp = context.getSharedPreferences(
getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
String saveValue = sp.getString(KEY_DEX2_SHA1, "");
return !StringUtils.equals(flag,saveValue);
}
//Get classes.dex file signature
private String get2thDexSHA1(Context context) {
ApplicationInfo ai = context.getApplicationInfo();
String source = ai.sourceDir;
try {
JarFile jar = new JarFile(source);
java.util.jar.Manifest mf = jar.getManifest();
Map<String, Attributes> map = mf.getEntries();
Attributes a = map.get("classes2.dex");
return a.getValue("SHA1-Digest");
} catch (Exception e) {
e.printStackTrace();
}
return null ;
}
/*****************************阻塞等待********************************/
/**
* 1. 启动 异步dexopt的跨进程Activity
* 2. 阻塞当前主进程
* 3. 200ms间隔轮训dexopt是否完成,超时或者已完成,则唤醒主进程
* 3.1 在attachContext中的MultixDex.install,如果超时,则主进程自己再次同步执行dexopt,相当于恢复到官方默认方案;
* 3.2 在attachContext中的MultixDex.install,如果已完成,则主进程不再执行的dexopt,单纯加载odex,提升速度
*/
public void waitForDexopt(Context base) {
Intent intent = new Intent();
ComponentName componentName = new
ComponentName(BaseFunctionConfig.PACKAGE_NAME, LoadResActivity.class.getName());
intent.setComponent(componentName);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
base.startActivity(intent);
long startWait = System.currentTimeMillis ();
long waitTime = 10 * 1000 ;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) {
waitTime = 20 * 1000 ;//实测发现某些场景下有些2.3版本有可能10s都不能完成optdex
}
while (needWait(base)) {
//application启动了LoadDexActivity之后,自身不再是前台进程所以怎么hold 线程都不会ANR
try {
long nowWait = System.currentTimeMillis() - startWait;
LogCore.i( TAG, "wait ms :" + nowWait);
if (nowWait >= waitTime) {
return;
}
Thread.sleep(200 );
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/*****************************utils********************************/
//LoadResActivity 中被调用
public void installFinish(Context context){
SharedPreferences sp = context.getSharedPreferences(
getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
}
public static PackageInfo getPackageInfo(Context context){
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
LogCore.i(TAG, e.getLocalizedMessage());
}
return new PackageInfo();
}
public static String getCurProcessName(Context context) {
try {
int pid = android.os.Process.myPid();
ActivityManager mActivityManager = (ActivityManager) context
.getSystemService(Context. ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
.getRunningAppProcesses()) {
if (appProcess.pid == pid) {
return appProcess. processName;
}
}
} catch (Exception e) {
// ignore
}
return null ;
}
}
- 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
public class LoadResActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super .onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN );
overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
setContentView(R.layout.activity_verify);
new LoadDexTask().execute();
}
class LoadDexTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
try {
MultiDex.install(getApplication());
LogUtils.d("loadDex" , "install finish" );
((App) getApplication()).installFinish(getApplication());
} catch (Exception e) {
LogUtils.e("loadDex" , e.getLocalizedMessage());
}
return null;
}
@Override
protected void onPostExecute(Object o) {
LogUtils.d( "loadDex", "get install finish");
finish();
System.exit( 0);
}
}
@Override
public void onBackPressed() {
//cannot backpress
}
Manifest.xml 里面
<activity
android:name= "com.zongwu.LoadResActivity"
android:launchMode= "singleTask"
android:process= ":mini"
android:alwaysRetainTaskState= "false"
android:excludeFromRecents= "true"
android:screenOrientation= "portrait" />
<activity
android:name= "com.zongwu.WelcomeActivity"
android:launchMode= "singleTop"
android:screenOrientation= "portrait">
<intent-filter >
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter >
</activity>
替换Activity默认的出现动画 R.anim.null_anim 文件的定义
-<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:fromAlpha="1.0"
android:toAlpha="1.0"
android:duration="550"/>
</set>
Application具有继承结构的注意,在子类中判断
if (isLoadDexProcess()) {
return;
}
因为有大量代码在MultiDex.install之前执行,因此必须把涉及到的class以显示声明的方式声明放到主dex中
//app/build.gradle
android {
//...
defaultConfig {
//...
//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
}
}
//app/mainDexClasses.pro
-keep class android.content.Intent { *; }
-keep interface android.content.SharedPreferences { *; }
//...自己看引用
这种方式好处在于依赖集非常简单,同时它的集成方式也是非常简单,我们无须去修改与加载无关的代码。
但是:使用“使用MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件”来判断是否dexopt完成的手段,在6.0上因为google废除了该标记导致行为准确性的难以保证
思路
使用“使用MODE_MULTI_PROCESS标记使得SharedPreference得以进程间共享,主进程轮询sp文件”来判断是否dexopt完成的手段,在6.0上因为google废除了该标记导致行为准确性的难以保证
针对该环节进行优化,通过 跨进程通讯的Messenger来实现
BaseApplication
public abstract class BaseApplication extends Application {
@SuppressWarnings("MismatchedReadAndWriteOfArray")
private static final byte[] lock = new byte[0];
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
//是<5.0的系统 && 是主进程(不是loaddex进程) 则进入异步加载方案
if (!isLoadDexProcess() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
//判断dexopt未执行过
if (needWait(base)) {
DexInstallDeamonThread thread = new DexInstallDeamonThread(this, base);
thread.start();
//阻塞等待:async_launch完成加载
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
thread.exit();
Log.d("BaseApplication", "dexopt finished. alloc MultiDex.install()");
} else {
//主进程加载dex,
//此时odex已经产生(dexopt操作已经在 loaddexActivity中执行过了,或者不是第一次打开应用),所以不会有耗时问题
MultiDex.install(this);
}
}else{
//>=5.0的系统默认对dex进行oat优化,不需要MultiDex.install (this );
return;
}
}
/*****************************判断是否是 loaddex进程********************************/
public boolean isLoadDexProcess() {
String processName = getCurProcessName(this);
return processName != null && processName.contains(":mini");
}
/****************************判断dexopt是否已经执行过********************************/
/**
* 另一种手段判断是否dexopt,将其换转为 判断是否是首次启动
* 这个标记应当随着版本升级而重置
* @param context
* @return
*/
public final static String IS_FIRST_LAUNCH = "";
@SuppressWarnings("deprecation")
private boolean needWait(Context context) {
//这里实现不唯一,读取一个全局的标记,判断是否初次启动APP
SharedPreferences sp = context.getSharedPreferences(
getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
return sp.getBoolean(IS_FIRST_LAUNCH, true);
}
/*****************************阻塞等待********************************/
/**
* 基于Messenger的跨进程通讯方式
* 1. 启动 异步dexopt 的跨进程Activity
* 2. 阻塞当前主进程
* 3. 锁机制等待dexopt是否完成
*/
private static class DexInstallDeamonThread extends Thread {
private Handler handler;
private Context application;
private Context base;
private Looper looper;
public DexInstallDeamonThread(Context application, Context base) {
this.application = application;
this.base = base;
}
@SuppressLint("HandlerLeak")
@Override
public void run() {
Looper.prepare();
looper = Looper.myLooper();
handler = new Handler() {
@SuppressWarnings("deprecation")
@Override
public void handleMessage(Message msg) {
synchronized (lock) {
lock.notify();
}
SPUtils
.getVersionSharedPreferences(application)
.edit()
.putBoolean(IS_FIRST_LAUNCH, false)
.apply();
}
};
Messenger messenger = new Messenger(handler);
Intent intent = new Intent(base, LoadResActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("MESSENGER", messenger);
base.startActivity(intent);
Looper.loop();
}
public void exit() {
if (looper != null) looper.quit();
}
}
/*****************************utils********************************/
public static String getCurProcessName(Context context) {
try {
int pid = android.os.Process.myPid();
ActivityManager mActivityManager = (ActivityManager) context
.getSystemService(Context. ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
.getRunningAppProcesses()) {
if (appProcess.pid == pid) {
return appProcess. processName;
}
}
} catch (Exception e) {
// ignore
}
return null ;
}
public static PackageInfo getPackageInfo(Context context){
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
Log.i("getPackaigeInfo", e.getLocalizedMessage());
}
return new PackageInfo();
}
}
LoadResActivity
public class LoadResActivity extends AppCompatActivity {
private Messenger messenger;
@Override
public void onCreate(Bundle savedInstanceState) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
setContentView(R.layout.activity_load_res);
Log.d("LoadResActivity", "start install");
Intent from = getIntent();
messenger = from.getParcelableExtra("MESSENGER");
LoadDexTask dexTask = new LoadDexTask();
dexTask.execute();
}
class LoadDexTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
try {
MultiDex.install(getApplication());
Log.d("LoadResActivity", "finish install");
messenger.send(new Message());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Void o) {
finish();
System.exit(0);
}
}
@Override
public void onBackPressed() {
//无法退出
}
}
app/AndroidManifest.xml加入:
<activity
android:name="com.synaric.dex.LoadResActivity"
android:launchMode= "singleTask"
android:alwaysRetainTaskState= "false"
android:excludeFromRecents= "true"
android:screenOrientation= "portrait"
android:process=":async_launch"/>
因为有大量代码在MultiDex.install之前执行,因此必须把涉及到的class以显示声明的方式声明放到主dex中
//app/build.gradle
android {
//...
defaultConfig {
//...
//定义main dex中必须保留的类
multiDexKeepProguard file('mainDexClasses.pro')
}
}
//app/mainDexClasses.pro
-keep public class * extends java.lang.Thread { *; }
-keep interface android.content.SharedPreferences { *; }
-keep class android.os.Handler { *; }
-keep class com.synaric.common.BaseSPKey { *; }
-keep class android.os.Messenger { *; }
-keep class android.content.Intent { *; }
//...自己看引用
该小节参考链接:
关于Android 64K引发的MultiDex你想知道的都在这里:一场由启动黑屏引发的惨案(非常详细,主要参考了这篇文章)
由于Multidex导致的Crash,adb logcat看下,基本也就是3类问题:
NoClassDefFoundError
ClassNotFoundException
Could not find method
所有这些都是同一个问题导致的:classes2.dex没加载完成之前,程序调用了classes2.dex中的类或者方法!
解决方案:将这些类,分包到MainDex里面。
怎么解决java.lang.NoClassDefFoundError错误
当你在项目中使用了multidex的时候,你的app可能会产生上述的异常。这意味着你的app在启动的时候没有找到含有指定类的class文件。
Android的Gradle插件首先需要SDK build tools 21.1及以上才支持multidex,它会在混淆工程之后列出一个主dex文件中包含的类的清单([buildDir]/intermediates/multi-dex/[buildType]/maindexlist.txt)。但这里面可能没有包含所有在App启动时需要加载的类,这时启动App就会抛出异常。
要解决这个问题,你要列出一份启动App时需要加载的类的清单,并告诉编译器这些类要保留在主dex文件中。你可以这么做:
android.applicationVariants.all { variant ->
task "fix${variant.name.capitalize()}MainDexClassList" << {
logger.info "Fixing main dex keep file for $variant.name"
File keepFile = new File("$buildDir/intermediates/multi-dex/$variant.buildType.name/maindexlist.txt")
keepFile.withWriterAppend { w ->
// Get a reader for the input file
w.append('\n')
new File("${projectDir}/multidex.keep").withReader { r ->
// And write data from the input into the output
w << r << '\n'
}
logger.info "Updated main dex keep file for ${keepFile.getAbsolutePath()}\n$keepFile.text"
}
}
}
tasks.whenTaskAdded { task ->
android.applicationVariants.all { variant ->
if (task.name == "create${variant.name.capitalize()}MainDexClassList") {
task.finalizedBy "fix${variant.name.capitalize()}MainDexClassList"
}
}
}
上述代码,参考自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包的配置使用中已经介绍过,配置如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.example.gao.delete"
minSdkVersion 17
targetSdkVersion 23
versionCode 1
versionName "1.0"
//1
multiDexEnabled true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
//2
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
def listFile = project.rootDir.absolutePath+'/app/maindexlist.txt'
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
//表示当方法数越界时则生成多个dex文件(我的没有越界,貌似也生成了两个)
dx.additionalParameters += '--multi-dex'
//这个指定了listFile中的类(即maindexlist.txt中的类)会打包到主dex中,不过注意下一条。
dx.additionalParameters += '--main-dex-list=' +listFile
//表明只有-main-dex-list所指定的类(在我的配置中,就是app目录下的maindexlist.txt中包含的类)才能打包到主dex中,如果没有这个选项,上个选项就会失效
dx.additionalParameters += '--minimal-main-dex'
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:24.0.0-alpha1'
//3
compile 'com.android.support:multidex:1.0.0'
}
根据上面builde.gradle中的配置,我们在app目录下创建一个maindexlist.txt,我们在这个txt里将我们想要放在主dex中的类写进去,自己写还是相当麻烦的,如果自己不会脚本自动生成的话,可以在\app\build\intermediates\multi-dex\debug目录下找到了一个maindexlist.txt,注意,这个你改了没用,一运行又恢复了,将这个复制到app目录下,然后将我们想要打包到MainDex中的类,按照要求的格式,添加进去即可。
《Android开发艺术探索》
在gradle.build的defaultConfig中可以指定一个文件来表示要放在第一个dex中的class
加入
multiDexKeepFile file('maindexlist.txt')
然后在gradle.build的同级目录下新建一个文件maindexlist.txt
里面填上要放在第一个dex中的class
如:
com/android/reverse/mod/ReverseXposedModule.class
top/imlk/xpmodulemultidexer/XMMultiDex.class
也可以用
multiDexKeepProguard file('maindexlist.pro')
然后
新建maindexlist.pro文件,这样就可以用proguard语法来写
-keep class com.android.reverse.mod.ReverseXposedModule
-keep class top.imlk.xpmodulemultidexer.*
可算是ok了。
Android实现Multidex及指定主dex中的class
第一次启动速度变慢和黑屏问题(严重ANR)解决方案里面的分包方案。
精简maindex,对于启动不需要的代码,将其分包到Second dex中。
谈谈MultiDex启动优化(这里面也详细的讲解了如何分包的)
关于MultiDex的源码分析,建议阅读:MultiDex源码分析。
Android 上为啥会有65536的限制,解释下原因。
Android 官方是如何解决65536问题的?MultiDex 在打包阶段和 app 运行阶段分别做了什么?
使用 MultiDex 可能会造成什么问题?
使用 MultiDex 后首次启动 app 有什么优化方向吗?
如何将指定的 class 打进 mainDex ?
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 后首次启动 app 有什么优化方向吗?
阅读本文前面章节。
如何将指定的 class 打进 mainDex ?
阅读本文前面章节。
关于 65535 限制与 MultiDex 会在面试中被问到的问题可能都在这了
一些其他的参考链接:
Android MultiDex 实践:如何绕过那些坑?(MultiDex编译过程,讲解了分包的几个Task)
Android的multidex带来的性能问题-减慢app启动速度(很烂,不建议看)
Android Multidex导致的App启动缓慢(详细的讲解了如何分包的)
Android MultiDex;NoClassDefFoundError;5.0以下系统应用闪退
MultiDex 编译过程(相当详细的讲解了Multidex的工作原理)