背景java
隨着tencentmap項目的愈來愈龐大,終於有一天咱們的App在Android 2.*如下手機上安裝時出現INSTALL_FAILED_DEXOPT,致使安裝失敗。android
INSTALL_FAILED_DEXOPT致使沒法安裝的問題,從根本上來講,多是兩個緣由形成的:app
(1)單個dex文件方法總數65K的限制。ide
(2)Dexopt的LinearAlloc限制。函數
當Android系統安裝一個應用的時候,有一步是對Dex進行優化,這個過程有一個專門的工具來處理,叫DexOpt。DexOpt是在第一次加載Dex文件的時候執行的。這個過程會生成一個ODEX文件,即Optimised Dex。執行ODEX的效率會比直接執行Dex文件的效率要高不少。工具
可是在早期的Android系統中,DexOpt有兩個問題。佈局
(1)DexOpt會把每個類的方法id檢索起來,存在一個鏈表結構裏面,可是這個鏈表的長度是用一個short類型來保存的,致使了方法id的數目不可以超過65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。優化
(2):Dexopt使用LinearAlloc來存儲應用的方法信息。Dalvik LinearAlloc是一個固定大小的緩衝區。在Android版本的歷史上,LinearAlloc分別經歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩衝區只有5MB,Android 4.x提升到了8MB或16MB。當方法數量過多致使超出緩衝區大小時,也會形成dexopt崩潰。this
儘管在新版本的Android系統中,DexOpt修復了方法數65K的限制問題,而且擴大了LinearAlloc限制,可是咱們仍然須要對低版本的Android系統作兼容。spa
——p.s. 上面內容是個人一位同事jintaoli在研究dex分包的時候總結的,我以爲很詳細,所以在徵得他贊成後貼了上來,很是感謝
所以咱們決定去作「插件化」 這件事,將與核心地圖業務邏輯關聯不大的模塊進行插件化,從而來解決掉上述問題。
優點&原理
也有人可能會說,若是代碼中的方法數太多的話,是否能夠將native替換成H5呢,這樣不是也能夠解決問題嗎? 固然這也是能夠的,但得「插件化」相比較H5來講更有優點:
一、模塊間真正的解耦;
二、開發時能夠並行完成,更加高效;
三、按需加載,減小App內存;
四、插件動態升級,不用在發fix版本;
五、主App安裝包體積減少,升級時更節省流量。
爲了這麼多的好處,咱們至少也應該去嘗試一下插件化吧。
目前tencentmap採用動態加載Apk的方法。關於動態加載apk,理論上能夠用到的有DexClassLoader、PathClassLoader和URLClassLoader,咱們先來看看這三種方法的差異:
DexClassLoader :能夠加載文件系統上的jar、dex、apk
PathClassLoader :能夠加載/data/app目錄下的apk,所以只能加載已經安裝的apk
URLClassLoader :能夠加載jar,可是因爲dalvik不能直接識別jar,因此此方法在android中沒法使用。
所以咱們選擇更適合咱們的DexClassLoader。
具體實現是:宿主程序啓動一個代理的Activity,在這個Activity中經過dexClassLoader將插件App動態的加載進來,咱們拿到實例以後,經過反射的方法來執行插件中的接口,從而實現插件在宿主中運行。
實現
1、宿主程序
宿主的MainActiviy有一句話和一個按鈕,如圖:
點擊按鈕就會調轉到ProxyActivity,跳轉的過程當中,傳遞一個插件apk所在的路徑。代碼以下:
1 class GoPlugin implements OnClickListener 2 { 3 @Override 4 public void onClick(View arg0) 5 { 6 Intent intent = new Intent(); 7 intent.setClass(MainActivity.this, ProxyActivity.class); 8 intent.putExtra("ExtraDexPath", "/mnt/sdcard/plugin.apk"); 9 startActivity(intent); 10 } 11 }
緊接着,ProxyActivity被喚起,咱們看一下ProxyActivity中如何處理。代碼以下:
1 package com.bryan.host; 2 3 import java.lang.reflect.Constructor; 4 import java.lang.reflect.InvocationTargetException; 5 import java.lang.reflect.Method; 6 7 import dalvik.system.DexClassLoader; 8 9 import android.annotation.SuppressLint; 10 import android.app.Activity; 11 import android.content.pm.PackageInfo; 12 import android.content.pm.PackageManager; 13 import android.os.Bundle; 14 15 public class ProxyActivity extends Activity 16 { 17 /* 接收mainActivity傳來的*/ 18 protected String mExtraClass; 19 protected String mExtraDexPath; 20 21 /* classloder來的object*/ 22 protected Class<?> mlocaClass; 23 protected Object mobject; 24 25 26 @Override 27 protected void onCreate(Bundle savedInstanceState) 28 { 29 super.onCreate(savedInstanceState); 30 31 mExtraClass = getIntent().getStringExtra("ExtraClass"); 32 mExtraDexPath = getIntent().getStringExtra("ExtraDexPath"); 33 34 if (mExtraClass == null) 35 { 36 OpenDefaultActivity(); 37 } 38 else 39 { 40 OpenAppointActivity(mExtraClass); 41 } 42 } 43 44 /* 加載插件的主activity*/ 45 protected void OpenDefaultActivity() 46 { 47 PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(mExtraDexPath, PackageManager.GET_ACTIVITIES); 48 if ((packageInfo.activities != null) && (packageInfo.activities.length >0)) 49 { 50 mExtraClass = packageInfo.activities[0].name; 51 OpenAppointActivity(mExtraClass); 52 } 53 } 54 55 /* 加載插件的指定activity*/ 56 @SuppressLint("NewApi") protected void OpenAppointActivity(final String className) 57 { 58 File dexOutputDir = this.getDir("dex", 0); 59 final String dexOutputPath = dexOutputDir.getAbsolutePath(); 60 61 DexClassLoader dexClassLoader = new DexClassLoader(mExtraDexPath, dexOutputPath, null, ClassLoader.getSystemClassLoader() ); 62 63 try 64 { 65 66 mlocaClass = dexClassLoader.loadClass(className); 67 Constructor<?> localConstructor = mlocaClass.getConstructor(new Class[]{}); 68 mobject = localConstructor.newInstance(new Object[]{}); 69 70 /* 反射 調用插件中的設置代理 */ 71 Method setProxy = mlocaClass.getMethod("setProxy", new Class[] {Activity.class}); 72 setProxy.setAccessible(true); 73 setProxy.invoke(mobject, new Object[]{this}); 74 /* 反射告訴插件是被宿主調起的*/ 75 Method onCreate = mlocaClass.getDeclaredMethod("onCreate", new Class[] {Bundle.class}); 76 onCreate.setAccessible(true); 77 Bundle bundle = new Bundle(); 78 bundle.putInt("Host", 1); 79 onCreate.invoke(mobject, new Object[]{bundle}); 80 81 } catch (Exception e) 82 { 83 e.printStackTrace(); 84 } 85 } 86 }
從上面代碼不難看出,執行onCreate以後,首先會進入OpenDefaultActivity,其實這裏就是獲取了一下插件安裝包的主Activity,而後調用OpenAppointActivity(className),這裏面是真正動態加載的過程,代碼61到68行就是加載的過程。加載完畢以後,咱們就獲得了插件主Activity的Class對象和Object對象,所以利用這兩個對象進行反射。
反射一共調用了兩個方法:setProxy是將當前ProxyActivity的對象傳遞給插件,讓插件實際是在調用代理當中的內容,另外就是調用onCreate,由於咱們經過classloader加載進來插件工程的MainActiviy後,該類就變成了一個普通的類,啓動的過程當中它的生命週期函數就會失效,所以咱們須要反射的將onCreate執行,同時傳遞一個int值給插件讓插件知道,它本身是被宿主程序調用起來的,而不是本身起來的,這樣可讓插件針對這兩種不一樣的狀況作不一樣的處理(具體能夠看插件的代碼)。
2、插件程序
爲了讓ProxyActivity能夠接管插件中Activity的操做,咱們能夠定義一個基類BaseActivity來處理代理相關的事情,同時對是否使用的代理,作出兩種處理方式,這樣繼承了BaseActivity的Activity在使用的時候,仍是正常的使用,不會有感知,而BaseActivity就須要處理好插件工程獨立運行時和被代理運行時的區別。咱們能夠看看實現:
1 package com.bryan.plugin; 2 3 4 import android.app.Activity; 5 import android.content.Intent; 6 import android.content.res.AssetManager; 7 import android.content.res.Resources; 8 import android.os.Bundle; 9 import android.view.View; 10 import android.view.ViewGroup.LayoutParams; 11 12 public class BaseActivity extends Activity 13 { 14 /* 宿主工程中的代理Activity*/ 15 protected Activity mProxyActivity; 16 17 /* 判斷是被誰調起的,若是是宿主調起的爲1 */ 18 int Who = 0; 19 20 public void setProxy(Activity proxyActivity) 21 { 22 mProxyActivity = proxyActivity; 23 } 24 25 @Override 26 protected void onCreate(Bundle savedInstanceState) 27 { 28 if (savedInstanceState != null) 29 { 30 Who = savedInstanceState.getInt("Host", 0); 31 } 32 if (Who == 0) 33 { 34 super.onCreate(savedInstanceState); 35 mProxyActivity = this; 36 } 37 } 38 39 protected void StartActivityByProxy(String className) 40 { 41 if (mProxyActivity == this) 42 { 43 Intent intent = new Intent(); 44 intent.setClassName(this, className); 45 this.startActivity(intent); 46 } 47 else 48 { 49 Intent intent = new Intent(); 50 intent.setAction("android.intent.action.ProxyVIEW"); 51 intent.putExtra("ExtraDexPath", "/mnt/sdcard/plugin.apk"); 52 intent.putExtra("ExtraClass", className); 53 mProxyActivity.startActivity(intent); 54 } 55 } 56 57 58 /* 重寫幾個重要的添加布局的類 */ 59 @Override 60 public void setContentView(View view) 61 { 62 if (mProxyActivity == this) 63 { 64 super.setContentView(view); 65 } 66 else 67 { 68 mProxyActivity.setContentView(view); 69 } 70 } 71 72 73 @Override 74 public void addContentView(View view, LayoutParams params) 75 { 76 if (mProxyActivity == this) 77 { 78 super.addContentView(view, params); 79 } 80 else 81 { 82 mProxyActivity.addContentView(view, params); 83 } 84 } 85 86 87 @Override 88 public void setContentView(int layoutResID) 89 { 90 if (mProxyActivity == this) 91 { 92 super.setContentView(layoutResID); 93 } 94 else 95 { 96 mProxyActivity.setContentView(layoutResID); 97 } 98 99 } 100 101 102 @Override 103 public void setContentView(View view, LayoutParams params) 104 { 105 if (mProxyActivity == this) 106 { 107 super.setContentView(view, params); 108 } 109 else 110 { 111 mProxyActivity.setContentView(view, params); 112 } 113 } 114 115 }
從上面的代碼能夠看出,當插件程序是本身啓動的話,走入OnCreate,最終mProxyActivity就是BaseActivity,而當插件是被宿主調起的話,執行了setProxy()後,mProxyActivity實際上就是宿主工程中的ProxyActivity。所以後面的函數在實現的時候須要判斷一次,若是不是被宿主啓動,那麼還走原來的流程,若是是宿主啓動,走宿主中的該方法。這裏須要重點說明一下StartActivityByProxy(className)這個函數:
1 protected void StartActivityByProxy(String className) 2 { 3 if (mProxyActivity == this) 4 { 5 Intent intent = new Intent(); 6 intent.setClassName(this, className); 7 this.startActivity(intent); 8 } 9 else 10 { 11 Intent intent = new Intent(); 12 intent.setAction("android.intent.action.ProxyVIEW"); 13 intent.putExtra("ExtraDexPath", "/mnt/sdcard/plugin.apk"); 14 intent.putExtra("ExtraClass", className); 15 mProxyActivity.startActivity(intent); 16 } 17 }
根據函數名,咱們就能夠知道這個方法是用來啓動一個Activity的,當插件是自啓動的時候,Intent咱們採用顯式調用的方式,將咱們要啓動的Activity拉起。而當咱們是宿主代理啓動時,由於宿主和插件工程不在同一個工程,所以顯示調用是不行的,而隱式調用的方法,前提是必需要App安裝,可是咱們的插件動態加載技術是不須要安裝App,這個地方剛開始困擾了我很久,後來才明白,這裏的action須要配置在宿主的ProxyActivity中。這樣新起來的Activity依舊是被ProxyActivity代理,因此就造成了一個循環。
接下來實現插件工程的入口類,因爲宿主接管了插件後,插件的Context對象就變成了宿主的Context,而這樣的話咱們就沒有辦法去經過Context對象去獲取咱們的資源,所以入口類的UI佈局須要用代碼進行動態佈局,以下所示:
1 package com.bryan.plugin; 2 3 import android.content.Context; 4 import android.graphics.Color; 5 import android.os.Bundle; 6 import android.view.View; 7 import android.view.View.OnClickListener; 8 import android.view.ViewGroup.LayoutParams; 9 import android.widget.Button; 10 import android.widget.LinearLayout; 11 12 public class MainActivity extends BaseActivity { 13 14 @Override 15 protected void onCreate(Bundle savedInstanceState) { 16 super.onCreate(savedInstanceState); 17 //setContentView(R.layout.activity_main); 18 19 // 初始化處理佈局 20 InitView(); 21 } 22 23 private void InitView() 24 { 25 View view = CreateView(mProxyActivity); 26 mProxyActivity.setContentView(view); 27 } 28 29 private View CreateView(final Context context) 30 { 31 LinearLayout linearLayout = new LinearLayout(context); 32 33 linearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 34 linearLayout.setBackgroundColor(Color.parseColor("#F4F4D6")); 35 Button button = new Button(context); 36 button.setText("plugin button"); 37 linearLayout.addView(button, LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT); 38 39 button.setOnClickListener(new OnClickListener() { 40 @Override 41 public void onClick(View v) { 42 StartActivityByProxy("com.bryan.plugin.TestActivity"); 43 } 44 }); 45 return linearLayout; 46 } 47 48 49 }
代碼中42行StartActivityByProxy() 就是啓動一個新的Activity的方式,前面已經介紹是經過隱式調用調起ProxyActivity,而後動態加載com.bryan.plugin.TestActivity類的方法,這裏再也不贅述,咱們看下子Activity的實現吧:
1 package com.bryan.plugin; 2 3 import android.content.Context; 4 import android.os.Bundle; 5 import android.view.View; 6 import android.view.ViewGroup.LayoutParams; 7 import android.widget.LinearLayout; 8 import android.widget.TextView; 9 10 public class TestActivity extends BaseActivity 11 { 12 @Override 13 protected void onCreate(Bundle savedInstanceState) 14 { 15 super.onCreate(savedInstanceState); 16 mProxyActivity.setContentView(CreateView(mProxyActivity)); 17 } 18 19 private View CreateView(final Context context) 20 { 21 LinearLayout linearLayout = new LinearLayout(context); 22 23 linearLayout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 24 TextView textView = new TextView(context); 25 textView.setText("bryan test plugin"); 26 linearLayout.addView(textView); 27 return linearLayout; 28 } 29 }
一樣佈局採用的也是代碼來建立的。
結果
咱們下看看插件獨立運行時的情景:
再看看經過宿主程序拉起的效果:
能夠看到,被獨立運行和插件運行,執行效果是同樣的,可是因爲採用了反射,因此執行效率會略微下降,其次,咱們能夠看到應用的標題發生了改變,這也說明,儘管apk在宿主程序中被執行了,可是它畢竟是在宿主程序裏面執行的,因此它仍是屬於宿主程序的,所以apk未安裝被執行時其標題不是本身的。
改進
到此爲止,咱們已經實現了一個插件化的demo,可是仍然存在很多的問題:
一、資源沒法加載:因爲插件的Context對象已經被宿主程序接管,所以沒法經過Context對象得到插件的資源文件,所以沒法加載。
二、Activity的生命週期函數失效:宿主使用classloader加載插件進來後,插件的Activity就被當成了一個普通的類來處理,此時系統就再也不接管它的生命週期。
那麼咱們若是解決這樣的問題呢?看下文吧