【Android開發學習筆記】【高級】【隨筆】插件化——初探

背景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就被當成了一個普通的類來處理,此時系統就再也不接管它的生命週期。

  那麼咱們若是解決這樣的問題呢?看下文吧

相關文章
相關標籤/搜索