Android中方法数超限问题与启动优化详解
前言
最近写了篇有关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>
参考
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
下一篇: Wpf,Unity6