Android之Android apk動態加載機制的研究

轉載請註明出處:http://blog.csdn.net/singwhatiwanna/article/details/22597587 (來自singwhatiwanna的csdn博客)java

背景

問題是這樣的:咱們知道,apk必須安裝才能運行,若是不安裝要是也能運行該多好啊,事實上,這不是徹底不可能的,儘管它比較難實現。在理論層面上,咱們能夠經過一個宿主程序來運行一些未安裝的apk,固然,實踐層面上也能實現,不過這對未安裝的apk有要求。咱們的想法是這樣的,首先要明白apk未安裝是不能被直接調起來的,可是咱們能夠採用一個程序(稱之爲宿主程序)去動態加載apk文件並將其放在本身的進程中執行,本文要介紹的就是這麼一種方法,同時這種方法還有不少問題,尤爲是資源的訪問。由於將apk加載到宿主程序中去執行,就沒法經過宿主程序的Context去取到apk中的資源,好比圖片、文本等,這是很好理解的,由於apk已經不存在上下文了,它執行時所採用的上下文是宿主程序的上下文,用別人的Context是沒法獲得本身的資源的,不過這個問題貌似能夠這麼解決:將apk中的資源解壓到某個目錄,而後經過文件去操做資源,這只是理論上可行,實際上仍是會有不少的難點的。除了資源存取的問題,還有一個問題是activity的生命週期,由於apk被宿主程序加載執行後,它的activity其實就是一個普通的類,正常狀況下,activity的生命週期是由系統來管理的,如今被宿主程序接管了之後,如何替代系統對apk中的activity的生命週期進行管理是有難度的,不過這個問題比資源的訪問好解決一些,好比咱們能夠在宿主程序中模擬activity的生命週期併合適地調用apk中activity的生命週期方法。本文暫時不對這兩個問題進行解決,由於很難,本文僅僅對apk的動態執行機制進行介紹,儘管如此,聽起來仍是有點小激動,不是嗎?android

工做原理

以下圖所示,首先宿主程序會到文件系統好比sd卡去加載apk,而後經過一個叫作proxy的activity去執行apk中的activity。git

關於動態加載apk,理論上能夠用到的有DexClassLoader、PathClassLoader和URLClassLoader。github

DexClassLoader :能夠加載文件系統上的jar、dex、apkapp

PathClassLoader :能夠加載/data/app目錄下的apk,這也意味着,它只能加載已經安裝的apkide

URLClassLoader :能夠加載java中的jar,可是因爲dalvik不能直接識別jar,因此此方法在android中沒法使用,儘管還有這個類模塊化

關於jar、dex和apk,dex和apk是能夠直接加載的,由於它們都是或者內部有dex文件,而原始的jar是不行的,必須轉換成dalvik所能識別的字節碼文件,轉換工具可使用android sdk中platform-tools目錄下的dx工具

轉換命令 :dx --dex --output=dest.jar src.jar測試

示例

宿主程序的實現

1. 主界面很簡單,放了一個button,點擊就會調起apk,我把apk直接放在了sd卡中,至於先把apk從網上下載到本地再加載實際上是一個道理。this

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. @Override  
  2. public void onClick(View v) {  
  3.     if (v == mOpenClient) {  
  4.         Intent intent = new Intent(this, ProxyActivity.class);  
  5.         intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk");  
  6.         startActivity(intent);  
  7.     }  
  8.   
  9. }  

點擊button之後,proxy會被調起,而後加載apk並調起的任務就交給它了

2. 代理activity的實現(proxy)

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. package com.ryg.dynamicloadhost;  
  2.   
  3. import java.lang.reflect.Constructor;  
  4. import java.lang.reflect.Method;  
  5.   
  6. import dalvik.system.DexClassLoader;  
  7. import android.annotation.SuppressLint;  
  8. import android.app.Activity;  
  9. import android.content.pm.PackageInfo;  
  10. import android.os.Bundle;  
  11. import android.util.Log;  
  12.   
  13. public class ProxyActivity extends Activity {  
  14.   
  15.     private static final String TAG = "ProxyActivity";  
  16.   
  17.     public static final String FROM = "extra.from";  
  18.     public static final int FROM_EXTERNAL = 0;  
  19.     public static final int FROM_INTERNAL = 1;  
  20.   
  21.     public static final String EXTRA_DEX_PATH = "extra.dex.path";  
  22.     public static final String EXTRA_CLASS = "extra.class";  
  23.   
  24.     private String mClass;  
  25.     private String mDexPath;  
  26.   
  27.     @Override  
  28.     protected void onCreate(Bundle savedInstanceState) {  
  29.         super.onCreate(savedInstanceState);  
  30.         mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH);  
  31.         mClass = getIntent().getStringExtra(EXTRA_CLASS);  
  32.   
  33.         Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath);  
  34.         if (mClass == null) {  
  35.             launchTargetActivity();  
  36.         } else {  
  37.             launchTargetActivity(mClass);  
  38.         }  
  39.     }  
  40.   
  41.     @SuppressLint("NewApi")  
  42.     protected void launchTargetActivity() {  
  43.         PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(  
  44.                 mDexPath, 1);  
  45.         if ((packageInfo.activities != null)  
  46.                 && (packageInfo.activities.length > 0)) {  
  47.             String activityName = packageInfo.activities[0].name;  
  48.             mClass = activityName;  
  49.             launchTargetActivity(mClass);  
  50.         }  
  51.     }  
  52.   
  53.     @SuppressLint("NewApi")  
  54.     protected void launchTargetActivity(final String className) {  
  55.         Log.d(TAG, "start launchTargetActivity, className=" + className);  
  56.         File dexOutputDir = this.getDir("dex"0);  
  57.         final String dexOutputPath = dexOutputDir.getAbsolutePath();  
  58.         ClassLoader localClassLoader = ClassLoader.getSystemClassLoader();  
  59.         DexClassLoader dexClassLoader = new DexClassLoader(mDexPath,  
  60.                 dexOutputPath, null, localClassLoader);  
  61.         try {  
  62.             Class<?> localClass = dexClassLoader.loadClass(className);  
  63.             Constructor<?> localConstructor = localClass  
  64.                     .getConstructor(new Class[] {});  
  65.             Object instance = localConstructor.newInstance(new Object[] {});  
  66.             Log.d(TAG, "instance = " + instance);  
  67.   
  68.             Method setProxy = localClass.getMethod("setProxy",  
  69.                     new Class[] { Activity.class });  
  70.             setProxy.setAccessible(true);  
  71.             setProxy.invoke(instance, new Object[] { this });  
  72.   
  73.             Method onCreate = localClass.getDeclaredMethod("onCreate",  
  74.                     new Class[] { Bundle.class });  
  75.             onCreate.setAccessible(true);  
  76.             Bundle bundle = new Bundle();  
  77.             bundle.putInt(FROM, FROM_EXTERNAL);  
  78.             onCreate.invoke(instance, new Object[] { bundle });  
  79.         } catch (Exception e) {  
  80.             e.printStackTrace();  
  81.         }  
  82.     }  
  83.   
  84. }  

說明:程序不難理解,思路是這樣的:採用DexClassLoader去加載apk,而後若是沒有指定class,就調起主activity,不然調起指定的class。activity被調起的過程是這樣的:首先經過類加載器去加載apk中activity的類並建立一個新對象,而後經過反射去調用這個對象的setProxy方法和onCreate方法,setProxy方法的做用是將activity內部的執行所有交由宿主程序中的proxy(也是一個activity),onCreate方法是activity的入口,setProxy之後就調用onCreate方法,這個時候activity就被調起來了。

待執行apk的實現

1. 爲了讓proxy全面接管apk中全部activity的執行,須要爲activity定義一個基類BaseActivity,在基類中處理代理相關的事情,同時BaseActivity還對是否使用代理進行了判斷,若是不使用代理,那麼activity的邏輯仍然按照正常的方式執行,也就是說,這個apk既能夠按照執行,也能夠由宿主程序來執行。

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. package com.ryg.dynamicloadclient;  
  2.   
  3. import android.app.Activity;  
  4. import android.content.Intent;  
  5. import android.os.Bundle;  
  6. import android.util.Log;  
  7. import android.view.View;  
  8. import android.view.ViewGroup.LayoutParams;  
  9.   
  10. public class BaseActivity extends Activity {  
  11.   
  12.     private static final String TAG = "Client-BaseActivity";  
  13.   
  14.     public static final String FROM = "extra.from";  
  15.     public static final int FROM_EXTERNAL = 0;  
  16.     public static final int FROM_INTERNAL = 1;  
  17.     public static final String EXTRA_DEX_PATH = "extra.dex.path";  
  18.     public static final String EXTRA_CLASS = "extra.class";  
  19.   
  20.     public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW";  
  21.     public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk";  
  22.   
  23.     protected Activity mProxyActivity;  
  24.     protected int mFrom = FROM_INTERNAL;  
  25.   
  26.     public void setProxy(Activity proxyActivity) {  
  27.         Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity);  
  28.         mProxyActivity = proxyActivity;  
  29.     }  
  30.   
  31.     @Override  
  32.     protected void onCreate(Bundle savedInstanceState) {  
  33.         if (savedInstanceState != null) {  
  34.             mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL);  
  35.         }  
  36.         if (mFrom == FROM_INTERNAL) {  
  37.             super.onCreate(savedInstanceState);  
  38.             mProxyActivity = this;  
  39.         }  
  40.         Log.d(TAG, "onCreate: from= " + mFrom);  
  41.     }  
  42.   
  43.     protected void startActivityByProxy(String className) {  
  44.         if (mProxyActivity == this) {  
  45.             Intent intent = new Intent();  
  46.             intent.setClassName(this, className);  
  47.             this.startActivity(intent);  
  48.         } else {  
  49.             Intent intent = new Intent(PROXY_VIEW_ACTION);  
  50.             intent.putExtra(EXTRA_DEX_PATH, DEX_PATH);  
  51.             intent.putExtra(EXTRA_CLASS, className);  
  52.             mProxyActivity.startActivity(intent);  
  53.         }  
  54.     }  
  55.   
  56.     @Override  
  57.     public void setContentView(View view) {  
  58.         if (mProxyActivity == this) {  
  59.             super.setContentView(view);  
  60.         } else {  
  61.             mProxyActivity.setContentView(view);  
  62.         }  
  63.     }  
  64.   
  65.     @Override  
  66.     public void setContentView(View view, LayoutParams params) {  
  67.         if (mProxyActivity == this) {  
  68.             super.setContentView(view, params);  
  69.         } else {  
  70.             mProxyActivity.setContentView(view, params);  
  71.         }  
  72.     }  
  73.   
  74.     @Deprecated  
  75.     @Override  
  76.     public void setContentView(int layoutResID) {  
  77.         if (mProxyActivity == this) {  
  78.             super.setContentView(layoutResID);  
  79.         } else {  
  80.             mProxyActivity.setContentView(layoutResID);  
  81.         }  
  82.     }  
  83.   
  84.     @Override  
  85.     public void addContentView(View view, LayoutParams params) {  
  86.         if (mProxyActivity == this) {  
  87.             super.addContentView(view, params);  
  88.         } else {  
  89.             mProxyActivity.addContentView(view, params);  
  90.         }  
  91.     }  
  92. }  

說明:相信你們一看代碼就明白了,其中setProxy方法的做用就是爲了讓宿主程序可以接管本身的執行,一旦被接管之後,其全部的執行均經過proxy,且Context也變成了宿主程序的Context,也許這麼說比較形象:宿主程序其實就是個空殼,它只是把其它apk加載到本身的內部去執行,這也就更能理解爲何資源訪問變得很困難,你會發現好像訪問不到apk中的資源了,的確是這樣的,可是目前我尚未很好的方法去解決。
2. 入口activity的實現

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. public class MainActivity extends BaseActivity {  
  2.   
  3.     private static final String TAG = "Client-MainActivity";  
  4.   
  5.     @Override  
  6.     protected void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         initView(savedInstanceState);  
  9.     }  
  10.   
  11.     private void initView(Bundle savedInstanceState) {  
  12.         mProxyActivity.setContentView(generateContentView(mProxyActivity));  
  13.     }  
  14.   
  15.     private View generateContentView(final Context context) {  
  16.         LinearLayout layout = new LinearLayout(context);  
  17.         layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,  
  18.                 LayoutParams.MATCH_PARENT));  
  19.         layout.setBackgroundColor(Color.parseColor("#F79AB5"));  
  20.         Button button = new Button(context);  
  21.         button.setText("button");  
  22.         layout.addView(button, LayoutParams.MATCH_PARENT,  
  23.                 LayoutParams.WRAP_CONTENT);  
  24.         button.setOnClickListener(new OnClickListener() {  
  25.             @Override  
  26.             public void onClick(View v) {  
  27.                 Toast.makeText(context, "you clicked button",  
  28.                         Toast.LENGTH_SHORT).show();  
  29.                 startActivityByProxy("com.ryg.dynamicloadclient.TestActivity");  
  30.             }  
  31.         });  
  32.         return layout;  
  33.     }  
  34.   
  35. }  

說明:因爲訪問不到apk中的資源了,因此界面是代碼寫的,而不是寫在xml中,由於xml讀不到了,這也是個大問題。注意到主界面中有一個button,點擊後跳到了另外一個activity,這個時候是不能直接調用系統的startActivity方法的,而是必須經過宿主程序中的proxy來執行,緣由很簡單,首先apk本書沒有Context,因此它沒法調起activity,另外因爲這個子activity是apk中的,經過宿主程序直接調用它也是不行的,由於它對宿主程序來講是不可見的,因此只能經過proxy來調用,是否是感受很麻煩?可是,你還有更好的辦法嗎?

3. 子activity的實現

[java]  view plain copy 在CODE上查看代碼片 派生到個人代碼片
 
  1. package com.ryg.dynamicloadclient;  
  2.   
  3. import android.graphics.Color;  
  4. import android.os.Bundle;  
  5. import android.view.ViewGroup.LayoutParams;  
  6. import android.widget.Button;  
  7.   
  8. public class TestActivity extends BaseActivity{  
  9.   
  10.     @Override  
  11.     protected void onCreate(Bundle savedInstanceState) {  
  12.         super.onCreate(savedInstanceState);  
  13.         Button button = new Button(mProxyActivity);  
  14.         button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,  
  15.                 LayoutParams.MATCH_PARENT));  
  16.         button.setBackgroundColor(Color.YELLOW);  
  17.         button.setText("這是測試頁面");  
  18.         setContentView(button);  
  19.     }  
  20.   
  21. }  

說明:代碼很簡單,不用介紹了,同理,界面仍是用代碼來寫的。

運行效果

1. 首先看apk安裝時的運行效果

2. 再看看未安裝時被宿主程序執行的效果

說明:能夠發現,安裝和未安裝,執行效果是同樣的,差異在於:首先未安裝的時候因爲採用了反射,因此執行效率會略微下降,其次,應用的標題發生了改變,也就是說,儘管apk被執行了,可是它畢竟是在宿主程序裏面執行的,因此它仍是屬於宿主程序的,所以apk未安裝被執行時其標題不是本身的,不過這也能夠間接證實,apk的確被宿主程序執行了,不信看標題。最後,我想說一下這麼作的意義,這樣作有利於實現模塊化,同時還能夠實現插件機制,可是問題仍是不少的,最複雜的兩個問題:資源的訪問和activity生命週期的管理,期待你們有好的解決辦法,歡迎交流。

代碼下載:

https://github.com/singwhatiwanna/dynamic-load-apk

http://download.csdn.net/detail/singwhatiwanna/7121505

相關文章
相關標籤/搜索