Android插件化开发之运行未安装apk的activity
1、介紹
我們知道PathClassLoader是一個應用的默認加載器(而且他只能加載data/app/xxx.apk的文件),但是我們加載插件一般使用DexClassLoader加載器,所以這里就有問題了,其實如果對于開始的時候,每個人都會認為很簡單,很容易想到使用DexClassLoader來加載Activity獲取到class對象,在使用Intent啟動
?
2、替換LoadApk里面的mClassLoader
我們知道我們可以將我們使用的DexClassLoader加載器綁定到系統加載Activity的類加載器上就可以了,這個是我們的思路。也是最重要的突破點。下面我們就來通過源碼看看如何找到加載Activity的類加載器。加載Activity的時候,有一個很重要的類:LoadedApk.Java,這個類是負責加載一個Apk程序的,我們可以看一下他的源碼:
我們知道內部有個mClassLoader成員變量,我們只需要獲取它就可以了,因為它不是靜態的,所以我們需要先獲取LoadApk這個類的對象,我們再去
看看ActivityThread.java這個類
我們可以發現ActivityThread里面有個靜態的成員變量sCurrentActivityThread,然后還有一個ArrayMap存放Apk包名和LoadedApk映射關系的數據結構,我們通過反射來獲取mClassLoader對象。
如果對ActivityThread.java這個類不熟悉的可以看我這篇博客,http://blog.csdn.net/u011068702/article/details/53207039?(Android插件化開發之AMS與應用程序(客戶端ActivityThread、Instrumentation、Activity)通信模型分析)非常重要,是app程序的入口處。
?
3、實現具體代碼
? ? ?1)我們先需要一個測試apk,然后把這個測試的test.apk,放到手機sdcard里面去,關鍵代碼如下 package com.example.testapkdemo;import android.os.Bundle; import android.support.v7.app.ActionBarActivity; import android.view.View; import android.view.View.OnClickListener; import android.widget.Toast;public class MainActivity extends ActionBarActivity {public static View parentView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);if (parentView == null) {setContentView(R.layout.activity_main);} else {setContentView(parentView);}findViewById(R.id.button).setOnClickListener(new OnClickListener(){@Overridepublic void onClick(View v) {Toast.makeText(MainActivity.this, "我是來自插件", Toast.LENGTH_SHORT).show();}});}public void setView(View view) {this.parentView = view;} } 效果圖如下:?
接下來是我宿主代碼:
ReflectHelper.java ?這個是的我的反射幫助類
?
package com.example.dexclassloaderactivity;import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Arrays;/*** 反射輔助函數* @author**/ public class ReflectHelper {public static final Class<?>[] PARAM_TYPE_VOID = new Class<?>[]{};public static Object invokeStaticMethod(String className, String methodName, Class<?>[] paramTypes, Object...params) {try {Class<?> clazz = Class.forName(className);Method method = clazz.getMethod(methodName, paramTypes);method.setAccessible(true);return method.invoke(null, params);} catch (Exception e) {e.printStackTrace();}return null;}public static Object invokeMethod(String className, String methodName, Class<?>[] paramTypes, Object obj, Object...params) {try {Class<?> clazz = Class.forName(className);Method method = clazz.getMethod(methodName, paramTypes);method.setAccessible(true);return method.invoke(obj, params);} catch (Exception e) {e.printStackTrace();}return null;}public static Object getStaticField(String className, String fieldName) {try {Class<?> clazz = Class.forName(className);Field field = clazz.getDeclaredField(fieldName);field.setAccessible(true);return field.get(null);} catch (Exception e) {e.printStackTrace();}return null;}public static Object getField(String className, String fieldName, Object obj) {try {Class<?> clazz = Class.forName(className);Field field = clazz.getDeclaredField(fieldName);field.setAccessible(true);return field.get(obj);} catch (Exception e) {e.printStackTrace();}return null;}public static void setStaticField(String className, String fieldName, Object value) {try {Class<?> clazz = Class.forName(className);Field field = clazz.getDeclaredField(fieldName);field.setAccessible(true);field.set(null, value);} catch (Exception e) {e.printStackTrace();}}public static void setField(String className, String fieldName, Object obj, Object value) {try {Class<?> clazz = Class.forName(className);Field field = clazz.getDeclaredField(fieldName);field.setAccessible(true);field.set(obj, value);} catch (Exception e) {e.printStackTrace();}}public static Object createInstance(String className, Class<?>[] paramTypes, Object...params) {Object object = null;try {Class<?> cls = Class.forName(className);Constructor<?> constructor = cls.getConstructor(paramTypes);constructor.setAccessible(true);object = constructor.newInstance(params);}catch (Exception e) {e.printStackTrace();}return object;}/*** Locates a given field anywhere in the class inheritance hierarchy.** @param instance an object to search the field into.* @param name field name* @return a field object* @throws NoSuchFieldException if the field cannot be located*/public static Field findField(Object instance, String name) throws NoSuchFieldException {for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {try {Field field = clazz.getDeclaredField(name);if (!field.isAccessible()) {field.setAccessible(true);}return field;} catch (NoSuchFieldException e) {// ignore and search next}}throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());}/*** Locates a given field anywhere in the class inheritance hierarchy.** @param cls to search the field into.* @param name field name* @return a field object* @throws NoSuchFieldException if the field cannot be located*/public static Field findField2(Class<?> cls, String name) throws NoSuchFieldException {Class<?> clazz = null;for (clazz = cls; clazz != null; clazz = clazz.getSuperclass()) {try {Field field = clazz.getDeclaredField(name);if (!field.isAccessible()) {field.setAccessible(true);}return field;} catch (NoSuchFieldException e) {// ignore and search next}}throw new NoSuchFieldException("Field " + name + " not found in " + clazz);}/*** Locates a given method anywhere in the class inheritance hierarchy.** @param instance an object to search the method into.* @param name method name* @param parameterTypes method parameter types* @return a method object* @throws NoSuchMethodException if the method cannot be located*/public static Method findMethod(Object instance, String name, Class<?>... parameterTypes)throws NoSuchMethodException {for (Class<?> clazz = instance.getClass(); clazz != null; clazz = clazz.getSuperclass()) {try {Method method = clazz.getDeclaredMethod(name, parameterTypes);if (!method.isAccessible()) {method.setAccessible(true);}return method;} catch (NoSuchMethodException e) {// ignore and search next}}throw new NoSuchMethodException("Method " + name + " with parameters " +Arrays.asList(parameterTypes) + " not found in " + instance.getClass());} }
MyApplication.java 這個類用實現替換mClassLoader
?
?
package com.example.dexclassloaderactivity;import java.io.File; import java.lang.ref.WeakReference;import dalvik.system.DexClassLoader; import android.annotation.SuppressLint; import android.app.Application; import android.os.Environment; import android.util.ArrayMap; import android.util.Log;public class MyApplication extends Application{public static final String TAG = "MyApplication";public static final String AppName = "test.apk";public static int i = 0;public static DexClassLoader mClassLoader;@Overridepublic void onCreate() {Log.d(TAG, "替換之前系統的classLoader");showClassLoader();try {String cachePath = this.getCacheDir().getAbsolutePath();String apkPath = /*Environment.getExternalStorageState() + File.separator*/"/sdcard/"+ AppName;mClassLoader = new DexClassLoader(apkPath, cachePath,cachePath, getClassLoader()); loadApkClassLoader(mClassLoader);} catch (Exception e) {e.printStackTrace();}Log.d(TAG, "替換之后系統的classLoader");showClassLoader();}@SuppressLint("NewApi")public void loadApkClassLoader(DexClassLoader loader) {try {Object currentActivityThread = ReflectHelper.invokeMethod("android.app.ActivityThread", "currentActivityThread", new Class[] {},new Object[] {});String packageName = this.getPackageName();ArrayMap mpackages = (ArrayMap) ReflectHelper.getField("android.app.ActivityThread", "mPackages", currentActivityThread);WeakReference wr= (WeakReference)mpackages.get(packageName);Log.e(TAG, "mClassLoader:" + wr.get()); ReflectHelper.setField("android.app.LoadedApk", "mClassLoader", wr.get(), loader);Log.e(TAG, "load:" + loader); } catch (Exception e) {Log.e(TAG, "load apk classloader error:" + Log.getStackTraceString(e)); }}/*** 打印系統的classLoader*/public void showClassLoader() {ClassLoader classLoader = getClassLoader();if (classLoader != null){Log.i(TAG, "[onCreate] classLoader " + i + " : " + classLoader.toString());while (classLoader.getParent()!=null){classLoader = classLoader.getParent();Log.i(TAG,"[onCreate] classLoader " + i + " : " + classLoader.toString());i++;}}} }然后就是MainActivity.java文件,里面包含了下面另外一種方式,打開activity,所以我把函數??inject(DexClassLoader loader)先注釋掉
?
?
package com.example.dexclassloaderactivity;import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.Field;import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; import dalvik.system.DexClassLoader; import dalvik.system.PathClassLoader;public class MainActivity extends ActionBarActivity{public static final String TAG = "MainActivity";public static final String AppName = "test.apk";public static DexClassLoader mDexClassLoader = null;public static final String APPName = "test.apk";public TextView mText;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);findViewById(R.id.text2).setOnClickListener(new OnClickListener(){@Overridepublic void onClick(View v) {try { // inject(MyApplication.mClassLoader);String apkPath = Environment.getExternalStorageDirectory().toString() + File.separator + APPName;Class clazz = MyApplication.mClassLoader.loadClass("com.example.testapkdemo.MainActivity");Intent intent = new Intent(MainActivity.this, clazz);startActivity(intent);finish();} catch (Exception e) {Log.e(TAG, "name:" + Log.getStackTraceString(e));}}});}private void inject(DexClassLoader loader){ PathClassLoader pathLoader = (PathClassLoader) getClassLoader(); try { Object dexElements = combineArray( getDexElements(getPathList(pathLoader)), getDexElements(getPathList(loader))); Object pathList = getPathList(pathLoader); setField(pathList, pathList.getClass(), "dexElements", dexElements); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } private static Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { ClassLoader bc = (ClassLoader)baseDexClassLoader; return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { return getField(paramObject, paramObject.getClass(), "dexElements"); } private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj, value); } private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class<?> localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); int j = i + Array.getLength(arrayRhs); Object result = Array.newInstance(localClass, j); for (int k = 0; k < j; ++k) { if (k < i) { Array.set(result, k, Array.get(arrayLhs, k)); } else { Array.set(result, k, Array.get(arrayRhs, k - i)); } } return result; } }?
?
這里一定要注意,我們犯了3個錯,
1、test.apk的路徑寫錯了,下次寫文件路徑的時候,我們應該需要加上File file = new File(path); 用file.exist()函數來判斷是否存在
2、從sdcard卡里面讀取test.apk,沒加上權限,? ?<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
3、寫了Application,忘了在AndroidManifest.xml文件里面聲明。
我們還需要注意要加上開啟activity 在AndroidManifest.xml里面注冊,切記,希望下次不要再次犯錯。
AndroidManifest.xml文件如下:
?
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"package="com.example.dexclassloaderactivity"android:versionCode="1"android:versionName="1.0" ><uses-sdkandroid:minSdkVersion="11"android:targetSdkVersion="21" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/><applicationandroid:name="com.example.dexclassloaderactivity.MyApplication"android:allowBackup="true"android:icon="@drawable/ic_launcher"android:label="@string/app_name"android:theme="@style/AppTheme" ><activityandroid:name=".MainActivity"android:label="@string/app_name" ><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><activity android:name="com.example.testapkdemo.MainActivity"> </activity> </application></manifest>
運行結果如下:
?
?
然后這里又出現了插件資源和宿主資源沖突問題,我們后面再來研究。
?
4、合并PathClassLoader和DexClassLoader中的dexElements數組?
我們首先來看一下PathClassLoader和DexClassLoader類加載器的父類BaseDexClassloader的源碼:
(這里需要注意的是PathClassLoader和DexClassLoader類的父加載器是BaseClassLoader,他們的父類是BaseDexClassLoader)
這里有一個DexPathList對象,在來看一下DexPathList.java源碼:
Elements數組,我們看到這個變量他是專門存放加載的dex文件的路徑的,系統默認的類加載器是PathClassLoader,本身一個程序加載之后會釋放一個dex出來,這時候會將dex路徑放到里面,當然DexClassLoader也是一樣的,那么我們會想到,我們是否可以將DexClassLoader中的dexElements和PathClassLoader中的dexElements進行合并,然后在設置給PathClassLoader中呢?這也是一個思路。我們來看代碼:
?
/*** 以下是一種方式實現的* @param loader*/ private void inject(DexClassLoader loader){PathClassLoader pathLoader = (PathClassLoader) getClassLoader();try {Object dexElements = combineArray(getDexElements(getPathList(pathLoader)),getDexElements(getPathList(loader)));Object pathList = getPathList(pathLoader);setField(pathList, pathList.getClass(), "dexElements", dexElements);} catch (IllegalArgumentException e) {e.printStackTrace();} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (ClassNotFoundException e) {e.printStackTrace();} }private static Object getPathList(Object baseDexClassLoader)throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {ClassLoader bc = (ClassLoader)baseDexClassLoader;return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); }private static Object getField(Object obj, Class<?> cl, String field)throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {Field localField = cl.getDeclaredField(field);localField.setAccessible(true);return localField.get(obj); }private static Object getDexElements(Object paramObject)throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {return getField(paramObject, paramObject.getClass(), "dexElements"); } private static void setField(Object obj, Class<?> cl, String field,Object value) throws NoSuchFieldException,IllegalArgumentException, IllegalAccessException {Field localField = cl.getDeclaredField(field);localField.setAccessible(true);localField.set(obj, value); }private static Object combineArray(Object arrayLhs, Object arrayRhs) {Class<?> localClass = arrayLhs.getClass().getComponentType();int i = Array.getLength(arrayLhs);int j = i + Array.getLength(arrayRhs);Object result = Array.newInstance(localClass, j);for (int k = 0; k < j; ++k) {if (k < i) {Array.set(result, k, Array.get(arrayLhs, k));} else {Array.set(result, k, Array.get(arrayRhs, k - i));}}return result; }然后運行的時候把MyApplication.java文件里面的函數loadApkClassLoader(mClassLoader);注釋掉,然后把MainActivity.java文件里面的inject(MyApplication.mClassLoader)不要注釋,運行效果一樣。
?
?
總結:
我們在使用反射機制來動態加載Activity的時候,有兩個思路:
1>、替換LoadApk類中的mClassLoader變量的值,將我們動態加載類DexClassLoader設置為mClassLoader的值
2>、合并系統默認加載器PathClassLoader和動態加載器DexClassLoader中的dexElements數組
這兩個的思路原理都是一樣的:就是讓我們動態加載進來的Activity能夠具備正常的啟動流程和生命周期。
我們還沒解決資源沖突問題,后面再解決,有點復雜。
?
?
?
?
?
?
?
?
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的Android插件化开发之运行未安装apk的activity的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 字符串之括号的有效性
- 下一篇: Android之推荐看的Android源