Android中方法数超限问题与启动优化详解
作者:laocaixw 发布时间:2023-03-05 17:55:41
前言
最近写了篇有关Eclipse工程转Android Studio工程的文章,而导致公司项目需要转 AS 的直接原因,就是今天要写的主题–方法数超限,相信大多数 Android 项目的都会碰到这个问题。
传统的 Eclipse 解决方法数超限的办法,就是在 project.properties
中加上 dex.force.jumbo=true
,然后清理工程重新编译。但是,当方法数越来越多,这个方法也会解决不了问题,这个时候,就要用到 Google 官方给出的方案 MultiDex 了。
MultiDex 解决方案
一、 如果你的 minSdkVersion >= 21
,那么只要在模块的 build.gradle 中添加:
android {
defaultConfig {
...
multiDexEnabled true
}
...
}
二、 如果你的 minSdkVersion < 21
,那么只要在模块的 build.gradle 中添加:
android {
defaultConfig {
...
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.1'
}
然后,如果你没有写自己的 Application 类,那么你只要写上自己的 Application 类,并继承 MultiDexApplication ;如果你写过自己的 Application 类,并且/或者不希望 Application 类继承 MultiDexApplication ,那么你需要重写 Application 的 attachBaseContext 方法:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(context);
Multidex.install(this);
}
谷歌multiDex存在的问题
虽然谷歌的分包方案很简单,但是效果并不是那么好,谷歌本身也枚举了分包方案的缺点:
如果在主线程中执行MultiDex.install,加载second dex,因为加载从dex是同步的,会阻塞线程,second dex太大的话,有可能导致ANR
API Level 14之前,由于Dalvik LinearAlloc bug(问题22586,就是上文提到的LinearAlloc问题),很可能会出问题的
应用程序使用了multiedex配置的,会造成使用比较大的内存
对于应用程序比较复杂的,存在较多的library的项目。multidex可能会造成不同依赖项目间的dex文件函数相互调用,找不到方法
启动优化
官方的解决方案虽然简单,但是也存在一定的局限。比如,首次加载应用时,由于需要加载 DEX 文件,会消耗较多的时间,导致启动速度慢,影响用户体验,甚至很可能引发 ANR 。
针对加载 Dex 问题,美团技术团队是这样做的:精简主 Dex 包,应用启动起来后再异步加载第二个 Dex 包。这是一个很不错的想法,但是实现起来有一定的难度。需要编写脚本,区分哪些类要放在主 Dex 包中,而且一般项目中都会用到很多第三方 SDK,这很可能导致主 Dex 包的精简程度不能达到我们想要的状态。
当然,除此之外,还有更适合我们的方案,微信开发团队的解决思路就很有意思,他们逆了不少 APP,最后参考并改进了 Facebook 的解决方案。大概的思路就是,新开一个进程来执行 Multidex.install()
方法。
微信开发团队的思路实现起来也比较简单,下面直接上我的代码(顺便把启动体验也优化了~):
Application 中的 attachBaseContext 方法:
@Override
protected void attachBaseContext(Context context) {
super.attachBaseContext(context);
if (MultiDexUtils.isMainProcess(context)) { // 判断是否是主进程,避免重复执行
MultiDexUtils.setMainActivityStarted(this, false); // 保存本地数据,标记主页面是否已经开启
MultiDexUtils.setLoadDexActivityClosed(this, false); // 保存本地数据,标记加载Dex进程是否已经关闭
MultiDexUtils.startLoadDexActivity(context); // 打开加载 Dex 的进程页面,这样我们的APP就变成后台进程了
}
}
加载 Dex 的进程:
public class LoadDexActivity extends Activity {
private static final int MULTIDEX_ERROR = 0;
private static final int MULTIDEX_ACTIVITY_STARTED = 1;
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MULTIDEX_ERROR:
case MULTIDEX_ACTIVITY_STARTED: // 退出当前进程
MultiDexUtils.setLoadDexActivityClosed(getApplication());
finish();
System.exit(0);
break;
default:
break;
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_loaddex);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
}
new Thread() {
@Override
public void run() {
Message message = handler.obtainMessage();
long startTime = System.currentTimeMillis();
long timeout = 10 * 1000; // 加载超时时间
try {
MultiDex.install(getApplication());
Thread.sleep(500);
// 等待主界面启动
while (!MultiDexUtils.isMainActivityStarted(getApplication()) &&
(System.currentTimeMillis() - startTime) < timeout) {
Thread.sleep(200);
}
message.what = MULTIDEX_ACTIVITY_STARTED;
handler.sendMessage(message);
} catch (Exception e) {
message.what = MULTIDEX_ERROR;
handler.sendMessage(message);
}
}
}.start();
}
@Override
public void onBackPressed() {
//cannot backpress
}
}
Manifest 中 LoadDexActivity 的配置:
<activity
android:name=".LoadDexActivity"
android:alwaysRetainTaskState="false"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:process=":loadDex"
android:screenOrientation="portrait">
</activity>
主界面 onCreate 方法:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
MultiDexUtils.setMainActivityStarted(getApplication()); // 告诉loadDex进程,主界面已启动
...
}
MultiDexUtils 工具类:
public class MultiDexUtils {
public static final String KEY_ACTIVITY_STARTED = "activity-started-";
public static final String KEY_LOADDEX_CLOSED = "loaddex-closed-";
/**
* 当前进程是否是主进程
*/
public static boolean isMainProcess(Context context) {
return "com.***.***(进程名一般是包名)".equals(getCurProcessName(context));
}
/**
* 设置-主界面已经打开
*/
public static void setMainActivityStarted(Context context) {
setMainActivityStarted(context, true);
}
/**
* 设置-主界面是否已经打开
*/
public static void setMainActivityStarted(Context context, boolean b) {
SharedPreferences sp = context.getSharedPreferences("multidex", Context.MODE_MULTI_PROCESS);
sp.edit().putBoolean(KEY_ACTIVITY_STARTED + getPackageInfo(context).versionCode, b).commit();
}
/**
* 是否需要等待主界面
*/
public static boolean isMainActivityStarted(Context context) {
SharedPreferences sp = context.getSharedPreferences("multidex", Context.MODE_MULTI_PROCESS);
return sp.getBoolean(KEY_ACTIVITY_STARTED + getPackageInfo(context).versionCode, false);
}
/**
* 判断加载页面是否关闭
*/
public static boolean isLoadDexActivityClosed(Context context) {
SharedPreferences sp = context.getSharedPreferences("multidex", Context.MODE_MULTI_PROCESS);
return sp.getBoolean(KEY_LOADDEX_CLOSED + getPackageInfo(context).versionCode, false);
}
/**
* 设置加载页面已经关闭
*/
public static void setLoadDexActivityClosed(Context context) {
setLoadDexActivityClosed(context, true);
}
/**
* 设置-加载页面是否已经关闭
*/
public static void setLoadDexActivityClosed(Context context, boolean b) {
SharedPreferences sp = context.getSharedPreferences("multidex", Context.MODE_MULTI_PROCESS);
sp.edit().putBoolean(KEY_LOADDEX_CLOSED + getPackageInfo(context).versionCode, b).commit();
}
/**
* 开启等待页面,新的进程
*/
public static void startLoadDexActivity(Context context) {
Intent intent = new Intent();
ComponentName componentName = new ComponentName("com.***.***(包名)", LoadDexActivity.class.getName());
intent.setComponent(componentName);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* 获取进程名
*/
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;
}
/**
* 获取包信息
*/
private static PackageInfo getPackageInfo(Context context) {
PackageManager pm = context.getPackageManager();
try {
return pm.getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
// Log.i(TAG, e.getLocalizedMessage());
}
return new PackageInfo();
}
}
另一种启动优化方案
还有一种简单的启动优化方案,只能优化启动体验,并不能解决 ANR 问题。
在点击桌面图标启动应用时,给个背景图片,启动完成后,将背景设回空。
1.在入口 Activity 中加入主题背景
android:theme="@style/SplashTheme"
style.xml 中加入配置:
value:
<style name="SplashTheme" parent="@android:style/Theme.NoTitleBar">
<item name="android:background">@drawable/logo_splash</item>
</style>
value-v21:(解决21版本及以上的过度动画效果问题)
<style name="SplashTheme" parent="@android:style/Theme.NoTitleBar.Fullscreen">
<item name="android:windowBackground">@drawable/logo_splash</item>
</style>
2.将背景设回原样
@Override
public void setTheme(int resid) {
super.setTheme(R.style.CustomTransparent);
}
style.xml 配置如下:
<style name="CustomTransparent" parent="@android:style/Theme.Translucent">
<item name="android:background">@null</item>
<item name="android:windowBackground">@color/curve_floater_frameColor</item>
</style>
参考
其实你不知道 MultiDex 到底有多坑
美团 Android DEX 自动拆包及动态加载简介
Android 拆分与加载 Dex 的多种方案对比
配置方法数超过 64K 的应用
来源:http://blog.laocaixw.cn/2017/05/15/Android方法数超限问题探究与启动优化/


猜你喜欢
- 虽然闭包主要是函数式编程的玩意儿,而C#的最主要特征是面向对象,但是利用委托或lambda表达式,C#也可以写出具有函数式编程风味的代码。同
- 哈喽大家好啊,我是Hydra。Spring作为项目中不可缺少的底层框架,提供的最基础的功能就是bean的管理了。bean的注入相信大家都比较
- 大家好,这是 C# 9.0 新特性短系列的第 5 篇文章。弃元(Discards) 是在 C# 7.0 的时候开始支持的,它是一种人为丢弃不
- Gradle是一个基于Apache Ant和Apache Maven概念的项目自动化构建开源工具。它使用一种基于Groovy的特定领域语言(
- 最近在做wifi的相关的东西,打印WifiInfo的时候 无意间发现一个参数,改参数可以查看是否连接成功了指定wifi,但是这是隐藏的,遂将
- 这篇文章主要介绍了Spring Cloud Zuul添加过滤器过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学
- 题目:打印出所有的 "水仙花数 ",所谓 "水仙花数 "是指一个三位数,其各位数字立方和等于该数本身
- Java 使用IO流实现大文件的分割与合并文件分割应该算一个比较实用的功能,举例子说明吧比如说:你有一个3G的文件要从一台电脑Copy到另一
- summarydetail传统的Spring项目会有很多的配置文件,比如我们要使用Redis,一般除了对应的依赖的jar包我们还需要在app
- 线程组线程组可以批量管理线程和线程组对象。一级关联例子如下,建立一级关联。public class MyThread43 implement
- Java super关键字super 关键字与 this 类似,this 用来表示当前类的实例,super 用来表示父类。super 可以用
- 剪贴板是Windows操作系统中最常用的功能之一,它用来从一个应用程序向另一个应用程序传递数据,可以是文本,图象,甚至是程序对象。不过剪贴板
- 一个项目中肯定会存在很多共用的查询数据,对于这一部分的数据,没必要每一个用户访问时都去查询数据库,因此配置二级缓存将是非常必要的。Mybat
- C#写入xml文件1、XmlDocument1。我认为是最原始,最基本的一种:利用XmlDocument向一个XML文件里写节点,然后再利用
- 1、final修饰类被final修饰的类不能被继承,因此final类的成员方法也不能被覆写,被final关键字修饰的类没有子类,因此类的实现
- 获取最新插入数据的id原始方法读取最后一条的插入数据,但这样会造成如果两条数据同时插入,会并发出现错误SELECT * FROM admin
- 前言本文尝试从What、Why、How这三个角度来探索Java中的弱引用,帮助大家理解Java中弱引用的定义、基本使用场景和使用方法。一、&
- 本文实例为大家分享了Android实现滑动效果的具体代码,供大家参考,具体内容如下坐标系与视图坐标系相辅相成1、坐标系:描述了View在屏幕
- 前言开发传统Java WEB工程时,我们可以使用JSP页面模板语言,但是在SpringBoot中已经不推荐使用了。SpringBoot支持如
- 卫星菜单 ArcMenu 相信大家接触安卓,从新手到入门的过渡,就应该会了解到卫星菜单、抽屉、Xutils、Coolmenu、一些大神封装好