Android apk動態加載機制的研究
背景
問題是這樣的:我們知道,apk必須安裝才能運行,如果不安裝要是也能運行該多好啊,事實上,這不是完全不可能的,盡管它比較難實現。在理論層面上,我們可以通過一個宿主程序來運行一些未安裝的apk,當然,實踐層面上也能實現,不過這對未安裝的apk有要求。我們的想法是這樣的,首先要明白apk未安裝是不能被直接調起來的,但是我們可以采用一個程序(稱之為宿主程序)去動態加載apk文件并將其放在自己的進程中執行,本文要介紹的就是這么一種方法,同時這種方法還有很多問題,尤其是資源的訪問。因為將apk加載到宿主程序中去執行,就無法通過宿主程序的Context去取到apk中的資源,比如圖片、文本等,這是很好理解的,因為apk已經不存在上下文了,它執行時所采用的上下文是宿主程序的上下文,用別人的Context是無法得到自己的資源的,不過這個問題貌似可以這么解決:將apk中的資源解壓到某個目錄,然后通過文件去操作資源,這只是理論上可行,實際上還是會有很多的難點的。除了資源存取的問題,還有一個問題是activity的生命周期,因為apk被宿主程序加載執行后,它的activity其實就是一個普通的類,正常情況下,activity的生命周期是由系統來管理的,現在被宿主程序接管了以后,如何替代系統對apk中的activity的生命周期進行管理是有難度的,不過這個問題比資源的訪問好解決一些,比如我們可以在宿主程序中模擬activity的生命周期并合適地調用apk中activity的生命周期方法。本文暫時不對這兩個問題進行解決,因為很難,本文僅僅對apk的動態執行機制進行介紹,盡管如此,聽起來還是有點小激動,不是嗎?
工作原理
如下圖所示,首先宿主程序會到文件系統比如sd卡去加載apk,然后通過一個叫做proxy的activity去執行apk中的activity。
關于動態加載apk,理論上可以用到的有DexClassLoader、PathClassLoader和URLClassLoader。
DexClassLoader :可以加載文件系統上的jar、dex、apk
PathClassLoader :可以加載/data/app目錄下的apk,這也意味著,它只能加載已經安裝的apk
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從網上下載到本地再加載其實是一個道理。
@Override public void onClick(View v) { if (v == mOpenClient) { Intent intent = new Intent(this, ProxyActivity.class); intent.putExtra(ProxyActivity.EXTRA_DEX_PATH, "/mnt/sdcard/DynamicLoadHost/plugin.apk"); startActivity(intent); } }
點擊button以后,proxy會被調起,然后加載apk并調起的任務就交給它了
2. 代理activity的實現(proxy)
package com.ryg.dynamicloadhost; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import dalvik.system.DexClassLoader; import android.annotation.SuppressLint; import android.app.Activity; import android.content.pm.PackageInfo; import android.os.Bundle; import android.util.Log; public class ProxyActivity extends Activity { private static final String TAG = "ProxyActivity"; public static final String FROM = "extra.from"; public static final int FROM_EXTERNAL = 0; public static final int FROM_INTERNAL = 1; public static final String EXTRA_DEX_PATH = "extra.dex.path"; public static final String EXTRA_CLASS = "extra.class"; private String mClass; private String mDexPath; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDexPath = getIntent().getStringExtra(EXTRA_DEX_PATH); mClass = getIntent().getStringExtra(EXTRA_CLASS); Log.d(TAG, "mClass=" + mClass + " mDexPath=" + mDexPath); if (mClass == null) { launchTargetActivity(); } else { launchTargetActivity(mClass); } } @SuppressLint("NewApi") protected void launchTargetActivity() { PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo( mDexPath, 1); if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) { String activityName = packageInfo.activities[0].name; mClass = activityName; launchTargetActivity(mClass); } } @SuppressLint("NewApi") protected void launchTargetActivity(final String className) { Log.d(TAG, "start launchTargetActivity, className=" + className); File dexOutputDir = this.getDir("dex", 0); final String dexOutputPath = dexOutputDir.getAbsolutePath(); ClassLoader localClassLoader = ClassLoader.getSystemClassLoader(); DexClassLoader dexClassLoader = new DexClassLoader(mDexPath, dexOutputPath, null, localClassLoader); try { Class<?> localClass = dexClassLoader.loadClass(className); Constructor<?> localConstructor = localClass .getConstructor(new Class[] {}); Object instance = localConstructor.newInstance(new Object[] {}); Log.d(TAG, "instance = " + instance); Method setProxy = localClass.getMethod("setProxy", new Class[] { Activity.class }); setProxy.setAccessible(true); setProxy.invoke(instance, new Object[] { this }); Method onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class }); onCreate.setAccessible(true); Bundle bundle = new Bundle(); bundle.putInt(FROM, FROM_EXTERNAL); onCreate.invoke(instance, new Object[] { bundle }); } catch (Exception e) { e.printStackTrace(); } } }
說明:程序不難理解,思路是這樣的:采用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既可以按照執行,也可以由宿主程序來執行。
package com.ryg.dynamicloadclient; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup.LayoutParams; public class BaseActivity extends Activity { private static final String TAG = "Client-BaseActivity"; public static final String FROM = "extra.from"; public static final int FROM_EXTERNAL = 0; public static final int FROM_INTERNAL = 1; public static final String EXTRA_DEX_PATH = "extra.dex.path"; public static final String EXTRA_CLASS = "extra.class"; public static final String PROXY_VIEW_ACTION = "com.ryg.dynamicloadhost.VIEW"; public static final String DEX_PATH = "/mnt/sdcard/DynamicLoadHost/plugin.apk"; protected Activity mProxyActivity; protected int mFrom = FROM_INTERNAL; public void setProxy(Activity proxyActivity) { Log.d(TAG, "setProxy: proxyActivity= " + proxyActivity); mProxyActivity = proxyActivity; } @Override protected void onCreate(Bundle savedInstanceState) { if (savedInstanceState != null) { mFrom = savedInstanceState.getInt(FROM, FROM_INTERNAL); } if (mFrom == FROM_INTERNAL) { super.onCreate(savedInstanceState); mProxyActivity = this; } Log.d(TAG, "onCreate: from= " + mFrom); } protected void startActivityByProxy(String className) { if (mProxyActivity == this) { Intent intent = new Intent(); intent.setClassName(this, className); this.startActivity(intent); } else { Intent intent = new Intent(PROXY_VIEW_ACTION); intent.putExtra(EXTRA_DEX_PATH, DEX_PATH); intent.putExtra(EXTRA_CLASS, className); mProxyActivity.startActivity(intent); } } @Override public void setContentView(View view) { if (mProxyActivity == this) { super.setContentView(view); } else { mProxyActivity.setContentView(view); } } @Override public void setContentView(View view, LayoutParams params) { if (mProxyActivity == this) { super.setContentView(view, params); } else { mProxyActivity.setContentView(view, params); } } @Deprecated @Override public void setContentView(int layoutResID) { if (mProxyActivity == this) { super.setContentView(layoutResID); } else { mProxyActivity.setContentView(layoutResID); } } @Override public void addContentView(View view, LayoutParams params) { if (mProxyActivity == this) { super.addContentView(view, params); } else { mProxyActivity.addContentView(view, params); } } }
說明:相信大家一看代碼就明白了,其中setProxy方法的作用就是為了讓宿主程序能夠接管自己的執行,一旦被接管以后,其所有的執行均通過proxy,且Context也變成了宿主程序的Context,也許這么說比較形象:宿主程序其實就是個空殼,它只是把其它apk加載到自己的內部去執行,這也就更能理解為什么資源訪問變得很困難,你會發現好像訪問不到apk中的資源了,的確是這樣的,但是目前我還沒有很好的方法去解決。
2. 入口activity的實現
public class MainActivity extends BaseActivity { private static final String TAG = "Client-MainActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initView(savedInstanceState); } private void initView(Bundle savedInstanceState) { mProxyActivity.setContentView(generateContentView(mProxyActivity)); } private View generateContentView(final Context context) { LinearLayout layout = new LinearLayout(context); layout.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); layout.setBackgroundColor(Color.parseColor("#F79AB5")); Button button = new Button(context); button.setText("button"); layout.addView(button, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); button.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(context, "you clicked button", Toast.LENGTH_SHORT).show(); startActivityByProxy("com.ryg.dynamicloadclient.TestActivity"); } }); return layout; } }
說明:由于訪問不到apk中的資源了,所以界面是代碼寫的,而不是寫在xml中,因為xml讀不到了,這也是個大問題。注意到主界面中有一個button,點擊后跳到了另一個activity,這個時候是不能直接調用系統的startActivity方法的,而是必須通過宿主程序中的proxy來執行,原因很簡單,首先apk本書沒有Context,所以它無法調起activity,另外由于這個子activity是apk中的,通過宿主程序直接調用它也是不行的,因為它對宿主程序來說是不可見的,所以只能通過proxy來調用,是不是感覺很麻煩?但是,你還有更好的辦法嗎?
3. 子activity的實現
package com.ryg.dynamicloadclient; import android.graphics.Color; import android.os.Bundle; import android.view.ViewGroup.LayoutParams; import android.widget.Button; public class TestActivity extends BaseActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Button button = new Button(mProxyActivity); button.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); button.setBackgroundColor(Color.YELLOW); button.setText("這是測試頁面"); setContentView(button); } }
說明:代碼很簡單,不用介紹了,同理,界面還是用代碼來寫的。
運行效果
1. 首先看apk安裝時的運行效果
2. 再看看未安裝時被宿主程序執行的效果
說明:可以發現,安裝和未安裝,執行效果是一樣的,差別在于:首先未安裝的時候由于采用了反射,所以執行效率會略微降低,其次,應用的標題發生了改變,也就是說,盡管apk被執行了,但是它畢竟是在宿主程序里面執行的,所以它還是屬于宿主程序的,因此apk未安裝被執行時其標題不是自己的,不過這也可以間接證明,apk的確被宿主程序執行了,不信看標題。最后,我想說一下這么做的意義,這樣做有利于實現模塊化,同時還可以實現插件機制,但是問題還是很多的,最復雜的兩個問題:資源的訪問和activity生命周期的管理,期待大家有好的解決辦法,歡迎交流。
代碼下載:
https://github.com/singwhatiwanna/dynamic-load-apk
http://download.csdn.net/detail/singwhatiwanna/7121505