又到周末了,时间过的很快,今天我们来看一下Android中插件开发篇的最后一篇文章的内容:动态加载Activity(免安装运行程序),在上一篇文章中说道了,如何动态加载资源(应用换肤原理解析),没看过的同学,可以转战:
http://blog.csdn.net/jiangwei0910410003/article/details/47679843
当然,今天说道的内容还这这篇文章有关系。关于动态加载Activity的内容,网上也是有很多文章介绍了。但是他们可能大部分都是介绍通过代理的方式去实现的,所以今天我要说的加载会有两种方式:
1、使用反射机制修改类加载器
2、使用代理的方式
这两种方式都有各自的优缺点,我会在后面的文章详细解说。
首先来看一个例子:360安全卫士
在主界面有一个添加更多工具的菜单,点进去之后,可以看到有很多功能选项。我们添加一个手机防盗的功能:有一个进度条开始添加。那么我们如何知道他是使用动态加载的呢?我们可以去查看他的数据文件目录:
我们可以看到有两个目录,比较见名知意:
app_plugins_v3
app_plugins_v3_odex
第一个目录是存放需要动态加载的功能插件,第二个目录是存放加载之后释放的dex目录。
上面分析了360的动态加载Activity功能,下面我们就来实现以下这个功能吧:
不过我们还得了解一下Android中的类加载器的相关知识,这里就不做介绍了:我在这篇文章中详细介绍了类加载器:
http://blog.csdn.net/jiangwei0910410003/article/details/41384667
我们知道PathClassLoader是一个应用的默认加载器(而且他只能加载data/app/xxx.apk的文件),但是我们加载插件一般使用DexClassLoader加载器,所以这里就有问题了,其实如果对于开始的时候,每个人都会认为很简单,很容易想到使用DexClassLoader来加载Activity获取到class对象,在使用Intent启动,这个很简单呀?但是实际上并不是想象的这么简单。原因很简单,因为Android中的四大组件都有一个特点就是他们有自己的启动流程和生命周期,我们使用DexClassLoader加载进来的Activity是不会涉及到任何启动流程和生命周期的概念,说白了,他就是一个普普通通的类。所以启动肯定会出错。
所以我们知道了问题所在,解决起来也就简单了,但是我们这里还有两种思路去解决这个问题:
我们只要让加载进来的Activity有启动流程和生命周期就OK了,所以这里我们需要看一下一个Activity的启动过程。当然这里不会详细介绍一个Activity启动过程的,因为那个太复杂了,而且我也说不清楚,我们知道我们可以将我们使用的DexClassLoader加载器绑定到系统加载Activity的类加载器上就可以了,这个是我们的思路。也是最重要的突破点。下面我们就来通过源码看看如何找到加载Activity的类加载器。
加载Activity的时候,有一个很重要的类:LoadedApk.java,这个类是负责加载一个Apk程序的,我们可以看一下他的源码:
我们可以看到他内部有一个mClassLoader变量,他就是负责加载一个Apk程序的,那么我们只要获取到这个类加载器就可以了。他不是static的,所以我们还得获取一个LoadedApk对象。我们在去看一下另外一个类:ActivityThread.java的源码
这里我们可以看到ActivityThread类中有一个自己的static对象,然后还有一个ArrayMap存放Apk包名和LoadedApk映射关系的数据结构,那么我们分析清楚了,下面就来通过反射来获取mClassLoader对象吧。
友情提示:这里可能有些同学会困惑,怎么能够找到这个mClassLoader呢。我在这里因为是为了讲解内容,所以反过来找这个东西了,其实正常情况下,我们在找关于一个Apk或者是Activity的相关信息的时候,特别是启动流程的时候,我们肯定会去找:ActivityThread.java这个类,这个类是很重要很重要的,也是关键的突破口,它内部其实有很多信息的,所以,我们应该先去找这个ActivityThread,然后从这个类中发现信息,然后会找到了LoadedApk这个类。关于ActivityThread这个类,为何如此重要,我们可以在看看他的源码:
他有这个方法?这个方法看到很熟悉呀?他不是Java程序运行的入口main方法吗?是的,没错,所有的app程序的执行入口就是这里,所以以后有人问你Android中程序运行的入口是哪里?不要再说是Application的onCreate方法了,其实是ActivityThread中的main方法。
好的,回到主干上来,我们现在开始编写代码来实现反射获取mClassLoader类,然后将其替换成我们的DexClassLoader类,不多说了,看一下Demo工程结构:
PluginActivity1 ==》插件工程
DynamicActivityForClassLoader ==》宿主工程
其中PluginActivity1工程很简单啦,就一个Activiity:
package com.example.dynamicactivityapk; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Toast; 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 插件~~", Toast.LENGTH_LONG).show(); }}); } public static void setLayoutView(View view){ parentView = view; } }
我们编译这个工程,得到PluginActivity1.apk程序:
下面来看一下宿主工程
宿主工程其实最大的功能就是加载上面的PluginActivity1.apk,然后启动内部的MainActivity就可以了,这里的核心代码就是如何通过反射替换系统的mClassLoader类:
@SuppressLint("NewApi") private void loadApkClassLoader(DexClassLoader dLoader){ try{ String filesDir = this.getCacheDir().getAbsolutePath(); String libPath = filesDir + File.separator +"PluginActivity1.apk"; // 配置动态加载环境 Object currentActivityThread = RefInvoke.invokeStaticMethod( "android.app.ActivityThread", "currentActivityThread", new Class[] {}, new Object[] {});//获取主线程对象 String packageName = this.getPackageName();//当前apk的包名 ArrayMap mPackages = (ArrayMap) RefInvoke.getFieldOjbect( "android.app.ActivityThread", currentActivityThread, "mPackages"); WeakReference wr = (WeakReference) mPackages.get(packageName); RefInvoke.setFieldOjbect("android.app.LoadedApk", "mClassLoader", wr.get(), dLoader); Log.i("demo", "classloader:"+dLoader); }catch(Exception e){ Log.i("demo", "load apk classloader error:"+Log.getStackTraceString(e)); } }
这里有一个参数就是需要替换的DexClassLoader的,从外部传递过来,然后进行替换。我们看看外部定义的DexClassLoader类:
String filesDir = this.getCacheDir().getAbsolutePath(); String libPath = filesDir + File.separator +"PluginActivity1.apk"; Log.i("inject", "fileexist:"+new File(libPath).exists()); //loadResources(libPath); DexClassLoader loader = new DexClassLoader(libPath, filesDir,filesDir, getClassLoader());
下面我们来运行一下这个程序,首先我们将PluginActivity1.apk放到宿主工程的data/data/cache目录下:
运行程序,点击加载:
运行发现失败,说是插件中的那个MainActivity没有在AndroidManifest.xml中声明?不对呀,我们在插件工程中明明声明了呀,为何他还是提示没有声明呢?
哎,其实仔细想想原因很简单的,因为DexClassLoader加载插件Apk,不会将其xml中的内容加载进来,所以在插件中声明是没有任何用途的,必须在宿主工程中声明:
<activity android:name="com.example.dynamicactivityapk.MainActivity"> </activity>
我们在运行程序:
我们点击按钮,看效果啦。果然可以加载成功了啦啦啦。很开心了。
不过这里还是需要注意两个问题:
1>、因为要加载插件中的资源,所以需要调用loadResources方法
2>、在测试的过程中,发现插件工程中setContentView方法没有效果了。所以就在插件工程中定义一个static的方法,用来提前设置视图的。
好了,这里就介绍了一个如何使用反射机制来动态加载一个Activity了,但是到这里还没有结束呢?因为还要介绍另外一种方式来设置类加载器。
我们首先来看一下PathClassLoader和DexClassLoader类加载器的父类BaseDexClassloader的源码:
(这里需要注意的是PathClassLoader和DexClassLoader类的父加载器是BootClassLoader,他们的父类是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; }我们在运行宿主程序,发现发现也是可以的,这里就不演示了,效果都是一样的。
这里总结一下:
我们在使用反射机制来动态加载Activity的时候,有两个思路:
1>、替换LoadApk类中的mClassLoader变量的值,将我们动态加载类DexClassLoader设置为mClassLoader的值
2>、合并系统默认加载器PathClassLoader和动态加载器DexClassLoader中的dexElements数组
这两个的思路原理都是一样的:就是让我们动态加载进来的Activity能够具备正常的启动流程和生命周期。
项目下载地址:http://download.csdn.net/detail/jiangwei0910410003/9063377
首先我们也是先来看一个例子:23Code
这个应用的功能就是实时的展示一些开源的UI控件。他是在线下载,然后动态加载进行展示的:
我们看到,点击运行Demo的时候,他会去下载apk,我们看看他的数据目录结构: