当前位置: 移动技术网 > 移动技术>移动开发>Android > Android中方法数超限问题与启动优化详解

Android中方法数超限问题与启动优化详解

2019年07月24日  | 移动技术网移动技术  | 我要评论

前言

最近写了篇有关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>

参考

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对移动技术网的支持。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网