其實你不知道MultiDex到底有多坑

前言:在android5.0之前,每一個android應用中只會含有一個dex文件,但是這個dex的方法數量被限制在65535之內,這就是著名的64K(64*1024)事件。為了解決這個問題,Google官方推出了這個類似于補丁一樣的support-library,MultiDex。上一篇文章我們已經了解了Multidex的使用及原理,詳見Android使用Multidex突破64K方法數限制原理解析。本篇文章我會將在使用Multidex的過程中遇到的一些坑點進行總結。

MultiDex引發的問題

周二的晚上愉快地寫著Android代碼,往工程里引入了一個默默無聞的jar然后Run了一下, 經過漫長的等待AndroidStudio構建失敗了。wtf ? 發生了什么?
emmm......帶著疑惑查看錯誤信息:

UNEXPECTED TOP-LEVEL EXCEPTION: java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536 
    at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501) 
    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276) 
    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490) 
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167) 
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:188) 
    at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439) 
    at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287) 
    at com.android.dx.command.dexer.Main.run(Main.java:230) 
    at com.android.dx.command.dexer.Main.main(Main.java:199) 
    at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED

看起來是:在試圖將 classes和jar塞進一個Dex文件的過程中產生了錯誤。

早期的Dex文件保存所有classes的方法個數的范圍在0~65535之間。業務一直在增長,寫(copy)的代碼越來越長引入的庫越來越多,超過這個范圍只是時間問題。這個問題怎么破??太陽底下木有新鮮事,淡定先google一發,找找已經踩過坑的小伙伴。StackOverflow 的網友們對該問題表示情緒穩定,談笑間拋出multiDex

我們來看看Android官方文檔對此是如何解釋的:

1.Dalvik Executable (DEX)文件的總方法數限制在65536以內,其中包括Android framwork method, lib method (后來發現僅僅是Android 自己的框架的方法就已經占用了1w多),還有你的 code method ,所以請使用MultiDex。
2.對于5.0以下版本,請使用multidex support library (這個是我們的補丁包!build tools 請升級到21)。
3.而5.0及以上版本,由于ART模式的存在,app第一次安裝之后會進行一次預編譯(pre-compilation) ,如果這時候發現了classes(..N).dex文件的存在就會將他們最終合成為一個.oat的文件(嗯看起來很厲害的樣子)。

同時Google建議review代碼的直接或者間接依賴,盡可能減少依賴庫,設置proguard參數進一步優化去除無用的代碼。嗯,這兩個實施起來倒是很簡單,但是治標不治本,躲得過初一躲不過十五。

在Google給出這個解決方案之前,他們的開發人員先給了一個簡易版本的multiDex。(懷疑后來的官方解決方案就有這家伙參與)。簡單地說就是:1.先把你的app 的class 拆分成主次兩個dex。2.你的程序運行起來后,自己把第二個dex給load進來。看就這么簡單!

第一回合 天真的官方補丁方案

還是先解決打包問題,回頭再研究那些高深的動態化加載技術。考慮到投入產出比,決定使用Google官方的multiDex解決。(Google的補丁方案啊,不會再有坑了吧?后面才發現還是太天真) 該方案有兩步:

  • 1.修改gradle腳本來產生多dex。
  • 2.修改manifest 使用MulitDexApplication。

步驟1.在gradle腳本里寫上:

android {
    compileSdkVersion 21
    buildToolsVersion "21.1.0"

    defaultConfig {
        ...
        minSdkVersion 14
        targetSdkVersion 21
        ...

        // Enabling multidex support.
        multiDexEnabled true
    }
    ...
}

dependencies {
compile 'com.android.support:multidex:1.0.0'
}

步驟2. manifest聲明修改

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.multidex.myapplication">
<application
...
android:name="android.support.multidex.MultiDexApplication">
...
</application>
</manifest>

如果有自己的Application,繼承MulitDexApplication。如果當前代碼已經繼承自其它Application沒辦法修改那也行,就重寫 Application的attachBaseContext()這個方法。

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);     
}

使用起來還是挺簡單的嘛, run一下,可以了!但是等等。。。dex過程好像變慢了。。。這是怎么肥事?

官方文檔還寫明了multiDex support lib 的使用局限。瞄一下是什么:

1.在應用安裝到手機上的時候dex文件的安裝是復雜的(complex)有可能會因為第二個dex文件太大導致ANR。請用proguard優化你的代碼。(呵呵...)
2.使用了mulitDex的App有可能在4.0(api level 14)以前的機器上無法啟動,因為Dalvik linearAlloc bug。請多多測試自祈多福。用proguard優化你的代碼將減少該bug幾率。(呵呵...)
3.使用了mulitDex的App在runtime期間有可能因為Dalvik linearAlloc limit Crash。該內存分配限制在 4.0版本被增大,但是5.0以下的機器上的Apps依然會存在這個限制。
4.主dex被dalvik虛擬機執行時候,哪些類必須在主dex文件里面這個問題比較復雜。build tools 可以搞定這個問題。但是如果你代碼存在反射和native的調用也不保證100%正確。(呵呵...)

感覺這就是個坑啊。補丁方案又引入一些其他問題。但是插件化方案要求對現有代碼有比較大的改動,代價太大,而且動態化加載框架意味著維護成本更高,會有更多潛在bug。所以先測試,遇到有問題的版本再解決。

第二回合 啥?dexopt failed?

呵呵,部分低端2.3機型(話說2.3版本的android機有高端機型么)安裝失敗!INSTALL_FAILED_DEXOPT。

apk是一個zip壓縮包,dalvik每次加載apk都要從中解壓出class.dex文件,加載過程還涉及到dex的classes需要的雜七雜八的依賴庫的加載,真耗時間。于是Android決定優化一下這個問題,在app安裝到手機之后,系統運行dexopt程序對dex進行優化,將dex的依賴庫文件和一些輔助數據打包成odex文件。存放在cache/dalvik_cache目錄下。保存格式為apk路徑 @ apk名 @ classes.dex。這樣以空間換時間大大縮短讀取/加載dex文件的過程。
那剛才那個bug是啥問題呢,原來dexopt程序的dalvik分配一塊內存來統計你的app的dex里面的classes的信息,由于classes太多方法太多超過這個linearAlloc 的限制 。那減小dex的大小就可以咯。

于是,我們來修改一下gradle腳本:

android.applicationVariants.all {
    variant ->
        dex.doFirst{
            dex->
            if (dex.additionalParameters == null) {
                dex.additionalParameters = []
            }
                dex.additionalParameters += '--set-max-idx-number=48000'
       }
}

--set-max-idx-number= 用于控制每一個dex的最大方法個數,如果寫小一點可能會產生好幾個dex。好了 現在2.3的機器可以安裝run起來了!

第三回合 ANR的意思就是Application Not Responding

問題又來了!這次不僅僅是2.3 的機型!還有一些中檔配置的4.x系統的機型。問題現象是:第一次安裝后,點擊圖標,1s,2s,3s... 程序沒有任何反應就好像你沒點圖標一樣。5s過去。。。程序ANR!

其實不僅僅這個App存在這個問題,其他很多App也存在首次安裝運行后幾秒都無任何響應的現象或者最后ANR了。唯一的例外是美團App,點擊圖標立馬就出現界面。唉要不就算啦?反正就一次。。。不行,這可是產品給用戶的第一印象啊太重要了,而且美團搞得定就說明這問題有解決方案。

ANR了是不是局限1描述的現象??不過也不重要...因為Google只是告訴你說第二個dex太大了導致的。并沒有進一步解釋根本原因。怎么辦?Google一發?搜索點擊圖標 然后ANR?怎么可能有解決方案嘛。ANR就意味著UI線程被阻塞了,老老實實查看log吧。
adb logcat -v time > log.txt于是發現 是 install dex + dexopt 時間太長!

梳理一下流程:

  • 安裝完app點擊圖標之后,系統木有發現對應的process,于是從該apk抽取classes.dex(主dex) 加載,觸發 一次dexopt。
  • App 的laucherActivity準備啟動 ,觸發Application啟動, Application的 onattach()方法調用,這時候MultiDex.install()調用,classes2.dex 被install,再次觸發dexopt。
  • 然后Applicaition onCreate()執行。然后 launcher Activity真的起來了。

這些必須在5s內完成不然就ANR給你看!有點棘手。

首先主dex是無論如何都繞不過加載和dexopt的。如果主dex比較小的話可以節省時間。主dex小就意味著后面的dex大啊,MultiDex.install()是在主線程里做的,總時間又沒有實質性改變。install() 能不能放到線程里做啊?貌似不行。。。如果異步化,什么時候install完成都不知道。這時候如果進程需要seconday.dex里的classes信息不就悲劇?主dex越小這個錯誤幾率就越大。要悲劇啊。

對于這個問題美團的主要思路是:精簡主dex+異步加載secondary.dex 。對異步化執行速度的不確定性,他們的解決方案是重寫Instrumentation execStartActivity 方法,hook跳轉Activity的總入口做判斷,如果當前secondary.dex 還沒有加載完成,就彈一個loading Activity等待加載完成,如果已經加載完成那最好不過了。

那我們照搬美團的解決方案不就好了咯?說是這么說, 但是在照搬方案之前,我們需要考慮以下幾個方面的問題:

  • 1..分析主dex需要的classes這個腳本比較難寫。。。
    Google文檔說過這個問題比較復雜, 而且buildTools 不是已經幫我們搞定了嗎?去瞄一下主dex的大小:8M 以及secondary.dex 3M 。 它是如何工作的?文檔說dx的時候,先依據manifest里注冊的組件生成一個 main-list,然后把這list里的classes所依賴的classes找出來,把他們打成classes.dex就是主dex。剩下的classes都放clsses2.dex(如果使用參數限制dex大小的話可能會有classe3.dex 等等) 。主dex至少含有main-list 的classes + 直接依賴classes ,使用mini-main-list參數可以僅僅包含剛才說的classes。
    關于寫分析腳本的思路是:直接使用mini-main-list參數獲取build目錄下的main-list文件,這樣manifest聲明的類和他們的直接依賴類搞定的了,那后者的直接依賴類怎么解?這些在dvk runtime也是必須的classes。一個思路是解析class文件獲得該class的依賴類。還一個思路是自己使用Dexclassloader 加載dex,然后hook getClass()方法,調用一次就記錄一個。都挺折騰的。

  • 2..由于歷史客觀原因,公司項目在維護的App的manifest注冊的組件的那些類,承載業務太多,依賴很多三方jar,導致直接依賴類非常多,而且短時間內無法梳理精簡,沒辦法mini化主dex。

  • 3..Application的啟動入口太多。。。
    Appication初始化未必是由launcher Activity的啟動觸發,還有可能是因為Service ,Receiver ,ContentProvider 的啟動。 靠攔截重寫Instrumentation execStartActivity 解決不了問題。要為 Service ,Receiver ,ContentProvider 分別寫基類,然后在oncreate()里判斷是否要異步加載secondary.dex。如果需要,彈出Loading Acitvity?用戶看到這個會感覺比較怪異。

結合自身App的實際情況來看美團的拆包方案雖然很美好然但是不能照搬啊。此時此刻,我的心情是拔涼拔涼的!┭┮﹏┭┮

第四回合 換一種思路解決

考慮到前面的種種困難, 還是不要寫分析腳本了吧。。畢竟投入產出嚴重失衡啦~~現在我們的問題變成了:既希望在Application的attachContext()方法里同步加載secondary.dex,又不希望卡住UI線程。如果思路限制在線程異步化上,確實不可能實現。

對此, FB的解決思路特別贊,讓Launcher Activity在另外一個進程啟動!當然這個Launcher Activity就是用來load dex 的 ,load完成就啟動Main Activity。
app在安裝完成之后第一次啟動時,是secondary.dex的dexopt花費了更多的時間,認識到這點非常重要,使得問題又轉化為:在不阻塞UI線程的前提下,完成dexopt,以后都不需要再次dexopt,所以可以在UI線程install dex 了!

因此,以下給出對FB解決方案的改進版:
先來看一下解決問題的思路流程圖:

MultiDex解決方案

上最終解決問題版的代碼!

  • 在Application里面(這里不要再繼承自MultiApplication了,我們要手動加載Dex):
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

public class App extends Application {
    // 標記
    public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
    @Override
    protected void attachBaseContext(Context base) {
        super .attachBaseContext(base);
        LogUtils.d( "loadDex", "App attachBaseContext ");
        //版本在5.0以下并且未執行過dexopt
        if (!quickStart() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {//>=5.0的系統默認對dex進行oat優化
            if (needWait(base)){ // 需要等待
                waitForDexopt(base); // 等待
            }
            MultiDex.install (this );
        } else {
            return;
        }
    }

    @Override
    public void onCreate() {
        super .onCreate();
        if (quickStart()) {
            return;
        }
        ...
    }

    // 是否執行過dexopt
    public boolean quickStart() {
        if (StringUtils.contains( getCurProcessName(this), ":mini")) {
            LogUtils.d( "loadDex", ":mini start!");
            return true;
        }
        return false ;
    }
    //是否需要等待dexopt完成
    private boolean needWait(Context context){
        String flag = get2thDexSHA1(context);
        LogUtils.d( "loadDex", "dex2-sha1 "+flag);
        SharedPreferences sp = context.getSharedPreferences(
                PackageUtil.getPackageInfo(context). versionName, MODE_MULTI_PROCESS);
        String saveValue = sp.getString(KEY_DEX2_SHA1, "");
        return !StringUtils.equals(flag,saveValue);
    }
    /**
     * 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 ;
    }
    // optDex 操作完成
    public void installFinish(Context context){
        SharedPreferences sp = context.getSharedPreferences(
                PackageUtil.getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
        sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
    }
    // 獲取當前進程名字
    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 ;
    }

    // 等待 進入LoadDexActivity 
    public void waitForDexopt(Context base) {
        Intent intent = new Intent();
        ComponentName componentName = new
                ComponentName( "com.zongwu", 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;
                LogUtils.d("loadDex" , "wait ms :" + nowWait);
                if (nowWait >= waitTime) {
                    return;
                }
                Thread.sleep(200 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其中PackageUtil的方法getPackageInfo

 public static PackageInfo getPackageInfo(Context context){
        PackageManager pm = context.getPackageManager();
        try {
            return pm.getPackageInfo(context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            LogUtils.e(e.getLocalizedMessage());
        }
        return  new PackageInfo();
    }

在Application啟動的時候會檢測dexopt是否已經完成過,(檢測方式是查看sp文件是否有dex文件的SHA1-Digest記錄,這里要兩個進程讀取該sp,讀取模式是MODE_MULTI_PROCESS)。如果沒有就啟動LoadDexActivity(屬于:mini進程) 。否則就直接install dex !對,直接install。通過日志發現,已經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.layout_load);      
        // 執行dexopt操作
        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 里面指定LoadResActivity啟動模式和運行進程
<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啟動了LoadDexActivity之后,自身不再是前臺進程所以怎么hold 線程都不會ANR。Perfect !!!

總結~~

OK. Multidex使用過程中遇到的坑,總算是完美解決了。感謝cctv...

  • MultiDex的問題難點在:要持續解決好幾個bug才能最終解決問題。進一步的,想要仔細分辨且解決這些bug,就必須持續探索一些關聯性的概念和原理。

  • 耗費了這么多時間來解決了Android系統的缺陷是不是有點略傷心。這不應該是Google給出一個比較徹底的解決方案嗎?

  • 時間不早了, 我該睡覺了~~ O(∩_∩)O哈哈~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 遭遇MultiDex 愉快地寫著Android代碼的總悟君往工程里引入了一個默默無聞的jar然后Run了一下, 經...
    尚妝楊逍閱讀 3,491評論 0 6
  • Tinker 熱補丁接入過程中的坑!!! =============== Tinker 介紹 官方接入說明 gra...
    朱立志閱讀 2,134評論 0 2
  • 原文鏈接 概述 作為一個android開發者,在開發應用時,隨著業務規模發展到一定程度,不斷地加入新功能、添加新的...
    goolong閱讀 1,868評論 2 5
  • 一、背景 隨著業務規模發展,不斷的加入新的功能,添加新的類庫,app的方法數已經超過65535,這樣的情況下就會遇...
    jiantao閱讀 18,436評論 6 50
  • 前言 最近開發中我們發現,我們的產品在Android設備版本低于5.0以下第一次安裝啟動會出現黑屏、ANR等情況。...
    miraclehen閱讀 3,589評論 2 11