本文说的关于动态加载的内容与之前一篇Android动态加载之插件开发有关系,本文要说的加载有两种:
- 使用反射机制修改类加载器
- 使用代理的方式
这两种方式都由各自的优缺点。
技术介绍
使用反射机制修改类加载器来实现动态加载Activity
我们知道PathClassLoader是一个应用的默认加载器(而且它只能加载data/app/xxx.apk的文件),但是我们加载插件一般使用DexClassLoader加载器,所以开始的时候,每个人都会很容易想到使用DexClassLoader来加载Activity获取到class对象,再使用Intent启动。但是实际上并不是想象的这么简单。因为Android中的四大组件都有一个特点就是他们有自己的启动流程和生命周期,我们使用DexClassLoader加载进来的Activity是不会涉及到任何启动流程和生命周期的概念,说白了,他就是一个普普通通的类。所以启动肯定会出错。
解决这个问题有两种思路:
思路1:替换LoadedApk中的mClassLoader
只要让加载进来的Activity有启动流程和生命周期就行了,所以这里需要看一下一个Activity的启动过程,当然不会详细介绍一个Activity启动过程的。可以将使用的DexClassLoader加载器绑定到系统加载Activity的类加载器上,这个是最重要的突破点。下面我们就来通过源码看看如何找到加载Activity的类加载器。
在找关于一个Apk或是Activity相关信息的时候,特别是启动流程的时候,肯定先找ActivityThread类,这个类很重要,也是关键突破口,它内部有很多的信息。
看一下ActivityThread.java的源码。
代码清单 /frameworks/base/core/java/android/app/ActivityThread.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | ... // set of instantiated backup agents, keyed by package name final ArrayMap<String, BackupAgent> mBackupAgents = new ArrayMap<String, BackupAgent>(); /** Reference to singleton {@link ActivityThread} */ private static ActivityThread sCurrentActivityThread; // 获取到对象 Instrumentation mInstrumentation; String mInstrumentationAppDir = null; String mInstrumentationAppLibraryDir = null; String mInstrumentationAppPackage = null; String mInstrumentedAppDir = null; String mInstrumentedAppLibraryDir = null; boolean mSystemThread = false; boolean mJitEnabled = false; // These can be accessed by multiple threads; mPackages is the lock. // XXX For now we keep around information about all packages we have // seen, not removing entries from this map. // NOTE: The activity and window managers need to call in to // ActivityThread to do things like update resource configurations, // which means this lock gets held while the activity and window managers // holds their own lock. Thus you MUST NEVER call back into the activity manager // or window manager or anything that depends on them while holding this lock. final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<String, WeakReference<LoadedApk>>(); // 获取到LoadedApk对象 final ArrayMap<String, WeakReference<LoadedApk>> mResourcePackages = new ArrayMap<String, WeakReference<LoadedApk>>(); final ArrayList<ActivityClientRecord> mRelaunchingActivities = new ArrayList<ActivityClientRecord>(); Configuration mPendingConfiguration = null; ... |
我们看到ActivityThread类中有一个自己的static对象,然后还有一个ArrayMap存放Apk包名和LoadedApk映射关系的数据结构。
LoadedApk.java是加载Activity的时候一个很重要的类,这个类是负责加载一个Apk程序的,我们可以看一下它的源码:
代码清单 /frameworks/base/core/java/android/app/LoadedApk.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * Local state maintained about a currently loaded .apk. * @hide */ public final class LoadedApk { private static final String TAG = "LoadedApk"; private final ActivityThread mActivityThread; private final ApplicationInfo mApplicationInfo; final String mPackageName; private final String mAppDir; private final String mResDir; private final String[] mSharedLibraries; private final String mDataDir; private final String mLibDir; private final File mDataDirFile; private final ClassLoader mBaseClassLoader; private final boolean mSecurityViolation; private final boolean mIncludeCode; private final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments(); Resources mResources; private ClassLoader mClassLoader; // 一个Apk加载的类加载器 private Application mApplication; private final ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers = new ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>(); private final ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mUnregisteredReceivers = new ArrayMap<Context, ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>(); private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices = new ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>>(); private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mUnboundServices = new ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>>(); int mClientCount = 0; Application getApplication() { return mApplication; } ... |
我们可以看到它内部有一个mClassLoader变量,就是负责加载一个Apk程序的,所以只要获取到这个类加载器就可以了。mClassLoader变量不是static的,所以我们还得获取一个LoadedApk对象。
关于ActivityThread类为何如此重要,我们可以看看它的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public static void main(String[] args) { SamplingProfilerIntegration.start(); // CloseGuard defaults to true and can be quite spammy. We // disable it here, but selectively enable it later (via // StrictMode) on debug builds, but using DropBox, not logs. CloseGuard.setEnabled(false); Environment.initForCurrentUser(); // Set the reporter for event logging in libcore EventLogger.setReporter(new EventLoggingReporter()); Security.addProvider(new AndroidKeyStoreProvider()); Process.setArgV0("<pre-initialized>"); Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } AsyncTask.init(); if (false) { Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread")); } |
它有Java程序运行的入口main方法,所有的APP程序的执行入口就是这里。
回到主干上来,我们现在开始编写代码实现反射获取mClassLoader类,然后将其替换成我们的DexClassLoader类,看一下项目结构:
- PluginActivity —— 插件项目
- DynamicActivityForClassLoader —— 宿主项目
PluginActivity项目很简单,就一个Activity:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | package com.example.dynamicactivityapk; public class MainActivity extends Activity { private static View parentView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(parentView == null) { setContentView(R.layout.activity_main); } else { setContentView(parentView); } findViewById(R.id.btn).setOnClickListener(new OnClickListener() { @Override public void onClick(View arg0) { Toast.makeText(getApplicationContext(), "I came from plugin~~", Toast.LENGTH_LONG).show(); }}); } public static void setLayoutView(View view){ parentView = view; } } |
编译PluginActivity项目:
下面看一下宿主项目,宿主项目其实最大的功能就是加载上面的PluginActivity.apk,然后启动内部的MainActivity就可以了,这里的核心代码就是如何通过反射替换系统的mClassLoader类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | @SuppressLint("NewApi") private void loadApkClassLoader(DexClassLoader dLoader) { try { // 配置动态加载环境 Object currentActivityThread = RefInvoke.invokeStaticMethod( "android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {}); // 获取ActivityThread对象 String packageName = this.getPackageName();// 当前apk的包名 ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mPackages"); WeakReference wr = (WeakReference) mPackages.get(packageName); // 获取到LoadedApk对象 RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader); // 设置LoadedApk中的mClassLoader的值为DexClassLoader Log.i("demo", "classloader:"+dLoader); } catch(Exception e) { Log.i("demo", "load apk classloader error:"+Log.getStackTraceString(e)); } } |
参数dLoader是需要替换的DexClassLoader,从外部传递过来,然后进行替换。我们看看外部定义的DexClassLoader类:
1 2 3 4 5 6 7 | String filesDir = this.getCacheDir().getAbsolutePath(); String libPath = filesDir + File.separator +"PluginActivity.apk"; Log.i("inject", "fileexist:"+new File(libPath).exists()); loadResources(libPath); DexClassLoader loader = new DexClassLoader(libPath, filesDir,filesDir, getClassLoader()); |
这里,需要注意的是,DexClassLoader的最后一个参数,是DexClassLoader的parent,这里需要设置成PathClassLoader类,因为我们上面虽然说是替换PathClassLoader为DexClassLoader。但是PathClassLoader是系统本身默认的类加载器(也就是mClassLoader变量的值,我们如果单独的将mClassLoader的值设置为DexClassLoader的话,就会出错的),所以一定要将DexClassLoader的父加载器设置成PathClassLoader,因为类加载器是符合双亲委派机制的。
运行程序之前要在宿主项目的AndroidManifest.xml中声明插件的MainActivity
1 2 3 | <activity android:name="com.example.dynamicactivityapk.MainActivity"> </activity> |
下面运行一下这个程序,首先将PluginActivity.apk放到宿主项目的cache目录下:
1 | $ adb push PluginActivity.apk /data/data/com.exmaple.testinjectdex/cache |
再点击按钮就能看到效果了。
- 因为要加载插件中的资源,所以需要调用loadResources方法
- 在测试过程中,发现插件项目中setContentView方法没有效果了。所以要在插件项目中定义一个static的方法,用来提前设置视图的。
思路2:合并PathClassLoader和DexClassLoader中的dexElements数组
下面介绍另外一种设置类加载器的方式。
先来看一下PathClassLoader和DexClassLoader类加载器的父类BaseDexClassLoader的源码:
(需要注意的是PathClassLoader和DexClassLoader类的父加载器是BootClassLoader,它们的父类是BaseDexClassLoader)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * Base class for common functionality between various dex-based * {@link ClassLoader} implementations. */ public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; /** * Constructs an instance. * * @param dexPath the list of jar/apk files containing classes and * resources, delimited by {@code File.pathSeparator}, which * defaults to {@code ":"} on Android * @param optimizedDirectory directory where optimized dex files * should be written; may be {@code null} * @param libraryPath the list of directories containing native * libraries, delimited by {@code File.pathSeparator}; may be * {@code null} * @param parent the parent class loader */ public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); } |
这里有一个DexPathList对象,再来看一下DexPathList.java源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | /** * A pair of lists of entries, associated with a {@code ClassLoader}. * One of the lists is a dex/resource path — typically referred * to as a "class path" — list, and the other names directories * containing native code libraries. Class path entries may be any of: * a {@code .jar} or {@code .zip} file containing an optional * top-level {@code classes.dex} file as well as arbitrary resources, * or a plain {@code .dex} file (with no possibility of associated * resources). * * <p>This class also contains methods to use these lists to look up * classes and resources.</p> */ /*package*/ final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String JAR_SUFFIX = ".jar"; private static final String ZIP_SUFFIX = ".zip"; private static final String APK_SUFFIX = ".apk"; /** class definition context */ private final ClassLoader definingContext; /** * List of dex/resource (class path) elements. * Should be called pathElements, but the Facebook app uses reflection * to modify 'dexElements' (http://b/7726934). */ private final Element[] dexElements; /** List of native library directories. */ private final File[] nativeLibraryDirectories; |
首先看一下这个类的描述,还有一个Elements数组,这个变量是专门存放加载的dex文件的路径的。系统默认的类加载器是PathClassLoader,本身一个程序加载之后会释放一个dex出来,这时候会将dex路径放到里面,当然DexClassLoader也是一样的,那么我们会想到,我们可以将DexClassLoader中的dexElements和PathClassLoader中的dexElements进行合并,然后再设置给PathClassLoader中。我们来看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | // 以下是另一种实现 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(bc, 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; } |
再运行宿主项目,可以正常运行,效果和前面一样。
这两个思路的原理都是为了让我们动态加载进来的Activity能够具备正常的启动流程和生命周期。
项目下载
使用静态代理的方式来动态加载Activity
原理:

这里的ProxyActivity就是代理对象,而每个插件Activity都是被代理对象,每个插件Activity对象中都会有一个代理Activity的对象引用,这个就是静态代理,原理就很简单了,就是插件Activity实际上不是真正意义上的Activity了,而是它们把所有Activity中的任务用代理对象Activity来执行了,所以我们只需要在宿主项目中声明一个ProxyActivity,然后用反射的方法设置到每个动态加载的Activity中去就可以了,比如:Plugin1中Activity执行setContentView方法的时候,其实就是执行ProxyActivity中的setContentView方法了。
因此,这种方式来加载Activity的话,其实真正意义上每个插件的Activity都不再是像方式一中的那样,没有声明周期,没有启动流程了,它们就是一个普通的Activity类,然后将其生命周期的所有任务都交给代理Activity去执行就可以了。
下面我们来看一下项目:
- DynamicActivityForProxy —— 宿主项目
- PluginActivity —— 插件项目
看一下插件项目:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package com.example.dynamicactivity; public class BaseActivity extends Activity { protected Activity mProxyActivity; public void setProxy(Activity proxyActivity) { mProxyActivity = proxyActivity; } @Override protected void onCreate(Bundle savedInstanceState) { } @Override public void setContentView(int layoutResID) { if (mProxyActivity != null && mProxyActivity instanceof Activity) { mProxyActivity.setContentView(layoutResID); mProxyActivity.findViewById(R.id.btn).setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { Toast.makeText(mProxyActivity, "我是插件,你是谁!",Toast.LENGTH_LONG).show(); } }); } } } |
这里重写了setContentView的方法,同时有一个setProxy方法。
再来看一下MainActivity.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onDestroy() { //这里需要注意的是,不能调用super.onDestroy的方法了,不然报错,原因也很简单,这个Activity实质上不是真正的Activity了,没有生命周期的概念了,调用super的方法肯定报错 Log.i("demo", "onDestory"); } @Override protected void onPause() { Log.i("demo", "onPause"); } @Override protected void onResume() { Log.i("demo", "onResume"); } @Override protected void onStart() { Log.i("demo", "onStart"); } @Override protected void onStop() { Log.i("demo", "onStop"); } } |
这里打印一下生命周期中的每个方法,待会需要验证。
运行插件项目,得到一个PluginActivity.apk。
下面看一下宿主项目:
首先看一下重要的代理对象ProxyActivity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | package com.example.dynamic.activity; public class ProxyActivity extends BaseActivity { private Object pluginActivity; private Class<?> pluginClass; private HashMap<String, Method> methodMap = new HashMap<String, Method>(); @SuppressLint("NewApi") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); try { DexClassLoader loader = initClassLoader(); //动态加载插件Activity pluginClass = loader.loadClass("com.example.dynamicactivity.MainActivity"); Constructor<?> localConstructor = pluginClass.getConstructor(new Class[] {}); pluginActivity = localConstructor.newInstance(new Object[] {}); //将代理对象设置给插件Activity Method setProxy = pluginClass.getMethod("setProxy",new Class[] { Activity.class }); setProxy.setAccessible(true); setProxy.invoke(pluginActivity, new Object[] { this }); initMethodMap(); //调用它的onCreate方法 Method onCreate = pluginClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class }); onCreate.setAccessible(true); onCreate.invoke(pluginActivity, new Object[] { new Bundle() }); } catch (Exception e) { Log.i("demo", "load activity error:" + Log.getStackTraceString(e)); } } /** * 存储每个生命周期的方法 */ private void initMethodMap(){ methodMap.put("onPause", null); methodMap.put("onResume", null); methodMap.put("onStart", null); methodMap.put("onStop", null); methodMap.put("onDestroy", null); for(String key : methodMap.keySet()){ try{ Method method = pluginClass.getDeclaredMethod(key); method.setAccessible(true); methodMap.put(key, method); }catch(Exception e){ Log.i("demo", "get method error:" + Log.getStackTraceString(e)); } } } @SuppressLint("NewApi") private DexClassLoader initClassLoader(){ String filesDir = this.getCacheDir().getAbsolutePath(); String libPath = filesDir + File.separator + "PluginActivity.apk"; Log.i("inject", "fileexist:"+new File(libPath).exists()); loadResources(libPath); DexClassLoader loader = new DexClassLoader(libPath, filesDir, null , getClass().getClassLoader()); return loader; } @Override protected void onDestroy() { super.onDestroy(); Log.i("demo", "proxy onDestroy"); try{ methodMap.get("onDestroy").invoke(pluginActivity, new Object[]{}); }catch(Exception e){ Log.i("demo", "run destroy error:" + Log.getStackTraceString(e)); } } @Override protected void onPause() { super.onPause(); Log.i("demo", "proxy onPause"); try{ methodMap.get("onPause").invoke(pluginActivity, new Object[]{}); }catch(Exception e){ Log.i("demo", "run pause error:" + Log.getStackTraceString(e)); } } @Override protected void onResume() { super.onResume(); Log.i("demo", "proxy onResume"); try{ methodMap.get("onResume").invoke(pluginActivity, new Object[]{}); }catch(Exception e){ Log.i("demo", "run resume error:" + Log.getStackTraceString(e)); } } @Override protected void onStart() { super.onStart(); Log.i("demo", "proxy onStart"); try{ methodMap.get("onStart").invoke(pluginActivity, new Object[]{}); }catch(Exception e){ Log.i("demo", "run start error:" + Log.getStackTraceString(e)); } } @Override protected void onStop() { super.onStop(); Log.i("demo", "proxy onStop"); try{ methodMap.get("onStop").invoke(pluginActivity, new Object[]{}); }catch(Exception e){ Log.i("demo", "run stop error:" + Log.getStackTraceString(e)); } } } |
这里主要就是:
- 加载插件Activity
- 使用反射将代理对象设置给插件Activity
- 测试插件Activity中的生命周期方法
运行程序,将上面的PluginActivity.apk放到宿主项目的cache目录下:
1 | $ adb push PluginActivity.apk /data/data/com.example.dynamic.activity/cache |
运行宿主项目就能看到效果了。同时我们打印一下Log:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | $ adb logcat -s demo ... I/demo (27983): proxy onStart I/demo (27983): onStart I/demo (27983): proxy onResume I/demo (27983): onResume I/demo (27983): proxy onPause I/demo (27983): onPause I/demo (27983): proxy onStop I/demo (27983): onStop I/demo (27983): proxy onDestroy I/demo (27983): onDestory I/demo (28565): proxy onStart I/demo (28565): onStart I/demo (28565): proxy onResume I/demo (28565): onResume I/demo (28565): proxy onPause I/demo (28565): onPause I/demo (28565): proxy onStop I/demo (28565): onStop I/demo (28565): proxy onDestroy I/demo (28565): onDestory |
插件Activity的每个生命周期的方法也是都执行了。
项目下载
两种方式的比较
第一种方式:使用反射机制来实现
优点:可以不用太多的关心插件中的Activity的生命周期方法,因为他加载进来之后就是一个真正意义上的Activity了
缺点:需要在宿主工程中进行声明,如果插件中的Activity多的话,那么就不灵活了。
第二种方式:使用代理机制来实现
优点:不需要在宿主工程中进行声明太多的Activity了,只需要有一个代理Activity的声明就可以了,很灵活
缺点:需要管理手动的去管理插件中Activity的生命周期方法,难度复杂。
reference
http://blog.csdn.net/jiangwei0910410003/article/details/48104455