应用方法数超过65535-Multidex的解决方案

Multidex的解决方案

今天写的内容主要是针对公司的项目中,方法数超过65535的问题,并给出解决方案。
本文是在前人的基础上稍作总结,具体见文末。

multidex起因

应用中的Dex 文件方法数超过了最大值65536的上限,简单来说,应用爆棚了,报错如下:

UNEXPECTED TOP-LEVEL EXCEPTION:
java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536

排查过程

出现这个问题很自然的想到了google给提出的补丁方案(本地由源码的话,在把网址改成sdk对应路径即可,例如:file:///C:/Android/sdk/docs/tools/building/multidex.html)

第一步:添加gradle配置(引入的multidex包,如果没网的情况下,请下载引入):
defaultConfig {
    applicationId "com.rytong.app.bankhx"
    minSdkVersion 8
    targetSdkVersion 8
    multiDexEnabled true
}

dependencies {
    compile 'com.android.support:multidex:1.0.1'
}
第二步:在Application中重写attachBaseContext方法,具体代码如下:
@Override
protected void attachBaseContext(Context base) {
    super .attachBaseContext(base);
    MultiDex.install (this);
}

到这个时候,我发现,运行项目app百分百ANR,百度后认为是google官方的补丁方案的bug,于是进行下面的第三步。

第三步:官方文档中提到的补丁方案的局限


大致意思如下:
1. 在安卓应用时,如果应用比较大,第二个dex文件过大,可能会导致ANR,请用ProGuard优化代码;
2. 使用了multidex的app可能在系统版本在4.0以下的手机启动失败,请多多测试,并使用ProGuard优化代码减少bug发生的几率;
3. 使用了mulitDex的App在runtime期间有可能因为Dalvik linearAlloc limit Crash。该内存分配限制在 4.0版本被增大,但是5.0以下的机器上的Apps依然会存在这个限制;
4. 在主dex执行时,需要保证一些类必须在主dex中,build tools 可以搞定这个问题。但是如果你代码存在反射和native的调用也不保证100%正确。

第四步:基于官方的文档中的限制,4.0以下的手机暂不做处理,4.0以上,5.0以下采用网上的方案,加载classes2.dex(原因是Facebook、美团、微信等团队实践后都认为在启动过程中classes2.dex加载占用时间过长),因此采用以下的方案,解决该问题。

由于代码较多,在本文档中不做过多阐述,以附件形式提供。在application启动时,启动一个新的activity加载classes2.dex,加载完之后再调用Application的onCreate方法启用应用。

具体流程图如下:

关键代码如下:

 @Override
protected void attachBaseContext(Context base) {
    super .attachBaseContext(base);
    Log.e( "attachBaseContext", "App attachBaseContext ");
    if (!quickStart() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {//>=5.0的系统默认对dex进行oat优化
        if (needWait(base)){
            Log.e( "attachBaseContext", "needWait");
            waitForDexopt(base);
        }
        Log.e( "attachBaseContext", "MultiDex.install");
        MultiDex.install (this );
    } else {
        return;
    }
}

public void waitForDexopt(Context base) {
    Intent intent = new Intent();
    ComponentName componentName = new
            ComponentName( "com.rytong.app.bankhx", "com.rytong.newengine.LoadResActivity");
//                ComponentName( "com.bank.hx", 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)) {
        try {
            long nowWait = System.currentTimeMillis() - startWait;
            Log.d("loadDex" , "wait ms :" + nowWait);
            if (nowWait >= waitTime) {
                return;
            }
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//neead wait for dexopt ?
private boolean needWait(Context context){
    String flag = get2thDexSHA1(context);
    Log.d( "loadDex", "dex2-sha1 "+flag);
    SharedPreferences sp = context.getSharedPreferences(
            PackageUtil.getPackageInfo(context). versionName, MODE_MULTI_PROCESS);
    String saveValue = sp.getString(KEY_DEX2_SHA1, "");
    Log.e( "needWait", flag+"..."+saveValue);
    if (!TextUtils.isEmpty(flag)) {
        return !flag.equals(saveValue);
    }else if(!TextUtils.isEmpty(saveValue)){
        return !saveValue.equals(flag);
    }
    return true;
}

/**
 * Get classes.dex file signature
 * @param context
 * @return
 */
private String get2thDexSHA1(Context context) {
    ApplicationInfo ai = context.getApplicationInfo();
    String source = ai.sourceDir;
    try {
        JarFile jar = new JarFile(source);
        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 ;
}



public boolean quickStart() {
    if (!TextUtils.isEmpty(getCurProcessName(this))) {
        Log.d( "loadDex", ":mini start!");
        return getCurProcessName(this).contains(":mini");
    }
    return false ;
}


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 ;
}

验证结果

第一步:在做完以上操作后,发现启动华夏银行app仍然100%ANR,我在想是不是因为我的方案没作用?因为我都输出日志了,并且执行到了MultiDex.install (this),报文都打印出来后ANR的。

第二步:排查应用启动Activity中是否做了阻塞线程的操作。

经排查发现,在启动Activity里面做了很多内容的初始化,比如

  • 初始化统计的接口
  • 开启推送服务
  • 开启hce推送
  • 注册广播或者一些监听
  • 打开ViewServer等

我将其中一部分注释掉后,app正常运行。在4.0以上5.0以下手机上,第一次安装或者覆盖安装第一次启动会显示loading页面3s左右,所以,能不用该方案则不用。(multidex方案大同小异,没有任何方案一点牺牲都不做的。

结论 

在经过此番排查后发现其实我们只需要按照google的标准做法即可,优化启动Activity中阻塞线程操作,就不会出现ANR(也就是说这个方案有可能都用不上)。
如果有65535问题,则建议项目上如此处理:
1. 按照google官方文档添加配置,在Application中添加相应代码;
2. 优化启动Activity中的阻塞线程操作,关闭或者放到异步线程执行。

附录

  1. 其实你不知道MultiDex到底有多坑:http://blog.zongwu233.com/the-touble-of-multidex
  2. 美团Android DEX自动拆包及动态加载简介:http://tech.meituan.com/mt-android-auto-split-dex.html
  3. Android拆分与加载Dex的多种方案对比:http://mp.weixin.qq.com/s?__biz=MzAwNDY1ODY2OQ==&mid=207151651&idx=1&sn=9eab282711f4eb2b4daf2fbae5a5ca9a&3rd=MzA3MDU4NTYzMw==&scene=6#rd
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值