本章来讲解一下Android插件化开发相关的知识。
第一节 基础知识
2015年是Android插件化技术突飞猛进的一年,随着业务的发展各大厂商都碰到了Android Native平台的瓶颈:
- 从技术上讲,业务逻辑的复杂导致代码量急剧膨胀,各大厂商陆续出到65535方法数的天花板;同时,运营为王的时代对于模块热更新提出了更高的要求。
- 在业务层面上,功能模块的解耦以及维护团队的分离也是大势所趋;各个团队维护着同一个App的不同模块,如果每个模块升级新功能都需要对整个app进行升级,那么发布流程不仅复杂而且效率低下;在讲究小步快跑和持续迭代的移动互联网必将遭到淘汰。
插件化技术听起来高深莫测,实际上要解决的就是两个问题:代码加载
和资源加载
。
代码加载
- 不同于Java的是,在Android中并不是说类加载进来就可以用了,很多组件都是有“生命”的(比如Activity);因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理;
- 另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?
资源加载
- 资源加载方案大家使用的原理都差不多,都是用AssetManager的隐藏方法addAssetPath。
- 但是这里面还是存在不少问题:如何正确的从多个插件中正确加载这些资源?如何处理插件与宿主的资源冲突?等等。
接下来,笔者通过几个简单的范例,来介绍如何实现动态加载代码和资源。
ClassLoader
笔者在《入门篇 第四章 数据存取》中简单的介绍了Classloader
的作用,本节将通过源码阅读,来看一下它的内部实现。
双亲委托机制
- 任何自定义ClassLoader都必须继承抽象类ClassLoader,并为其paren字段初始化。
- 任何自定义ClassLoader在加载一个类之前都会先委托其parent去加载,只有parent加载失败才会自己加载。
- 这样既可以防止重复加载,又可以排除安全隐患(防止用户替换系统核心类)。
范例1:ClassLoader
类。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public abstract class ClassLoader {
// 此处省略若干代码
private ClassLoader parent;
// 此处省略若干代码
ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
if (parentLoader == null && !nullAllowed) {
throw new NullPointerException("parentLoader == null && !nullAllowed");
}
parent = parentLoader;
}
// 此处省略若干代码
}
事实上,双亲委托机制是在loadClass()
方法实现的,如果你要想避开这个机制,必须重写loadClass()
方法。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
26protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// 首先查看该类是否已在缓存中了。
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
// 然后调用父类去执行加载的任务
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
// 最后才会由自己加载
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
语句解释:
- 从上面加载类的顺序中我们可以知道,loadClass会先看这个类是不是已经被loaded过,没有的话则去他的parent去找,如此递归,称之为双亲委托。
Android的类加载器
需要知道的是,Dalvik
虚拟机毕竟不算是标准的Java
虚拟机,因此在类加载机制上,它们有相同的地方,也有不同之处。我们必须区别对待。
- 因此Android为我们从ClassLoader派生出了两个类:DexClassLoader和PathClassLoader。
- 这两个类都属于符合双亲委派模型的类加载器(因为它们没有重写loadClass方法)。
- 其中DexClassLoader类,可以在运行时动态加载并解释执行包含在jar或apk文件内的dex文件,也是我们接下来要用到的类。
本节参考阅读:
加载代码
接下来我们使用DexClassLoader
来加载一个普通的jar
文件。
范例1:创建Person类。1
2
3
4
5
6
7package com.cutler.classload;
public class Person {
public static void say() {
System.out.println("Hello !!!");
}
}
语句解释:
- 在Eclipse创建一个新项目,然后在项目中创建这个Person类即可。
接着,将这个Person
导出成一个test.jar
文件,如果不会导出,请点击 这里 。
接着,将test.jar
上传到手机的SD卡根目下,并在Android项目中执行下面范例2
的代码。
范例2:动态加载Person类。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
28public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// test.jar在SD卡上的位置
File filePath = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/test.jar");
// test.jar文件的解压目录,即/data/data/packagename/app_dex/下面
File outputDir = getDir("dex", Context.MODE_PRIVATE);
// 创建一个DexClassLoader对象
DexClassLoader classLoader = new DexClassLoader(
filePath.getAbsolutePath(),
outputDir.getAbsolutePath(), null, getClassLoader());
try {
// 使用类加载器加载类文件
Class<?> clazz = classLoader.loadClass("com.cutler.classload.Person");
// 获取say方法
Method m = clazz.getMethod("say");
// 调用say方法,由于它是静态方法所以参数传递null即可
m.invoke(null);
} catch (Exception e) {
e.printStackTrace();
System.out.println(e.getMessage());
}
}
}
语句解释:
- 如果不出意外的话,这段代码并不会成功运行,而是会抛异常。
- 这是因为普通的jar文件是没法被DexClassLoader加载的,我们需要使用sdk里的dx工具优化一下jar文件才行。
范例3:dx工具。1
dx --dex --output=newtest.jar test.jar
语句解释:
- 将test.jar拷贝到dx工具所在的目录后,执行这条命令即可。
- dx工具在“android-sdk-windows\build-tools\选择一个版本\”下面。
然后再把newtest.jar
放到手机中就可以了。
安全提醒
需要注意的是,在实际开发的时候最好不要把test.jar
放到SD卡上,有两个原因:
- 首先,如果用户把SD卡上的test.jar文件给删除的话,程序就无法再加载了。
- 然后,SD卡上的test.jar文件是可以被任意程序修改的,因此它可能会遭到恶意程序的代码注入。
- 如果必须放到SD卡上的话,那每次加载之前最好对jar或dex文件做完整性校验。
最好的做法是把文件放到私有目录(/data/data/...)
或者直接放到apk
中。
本节参考阅读:
加载资源
加载资源的操作也很简单,需要用到ClassLoader
和AssetManager
类。
首先,来创建一个新的Android项目,包名为com.cutler.androidtest2
,并随便添加几个资源(如字符串、图片等)。
然后,打出apk
,并将apk
放到SD卡上。
范例1:加载未安装apk中的资源。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
62public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView img = (ImageView) findViewById(R.id.img);
// apk文件的路径
String pluginPath = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/app-debug.apk";
// 创建ClassLoader,稍后会用到。
ClassLoader classLoader = new DexClassLoader(
new File(pluginPath).getAbsolutePath(),
getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, getClassLoader());
// 新创建一个Resource对象。
Resources pluginRes = getPluginResource(pluginPath);
// 获取文本的资源id。
int resId = getPluginResourceId(classLoader, "com.cutler.androidtest2", "string", "text");
System.out.println(pluginRes.getString(resId));
// 获取图片的资源id。
resId = getPluginResourceId(classLoader, "com.cutler.androidtest2", "mipmap", "mv");
img.setImageDrawable(pluginRes.getDrawable(resId));
}
public Resources getPluginResource(String pluginPath) {
Resources pluginRes = null;
try {
// 创建一个新的AssetManager对象。
AssetManager assetManager = AssetManager.class.newInstance();
// 调用addAssetPath方法,将apk的路径放上去。
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginPath);
Resources superRes = super.getResources();
// 使用assetManager创建一个新的Resources对象。
pluginRes = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return pluginRes;
}
public int getPluginResourceId(ClassLoader classLoader, String packageName,
String resType, String name) {
int resId = 0;
try {
// 反射apk中的R文件,获取资源所对应的id。
Class clazz = classLoader.loadClass(packageName + ".R$" + resType);
Field property = clazz.getField(name);
// 由于资源都是静态的,所以这里传null。
resId = property.getInt(null);
} catch (Exception e) {
e.printStackTrace();
}
return resId;
}
}
语句解释:
- 默认的Resources对象只能加载当前APK中的资源,想要加载其他APK的资源,就需要手动创建一个Resources对象。
- 由于新Resources对象是由AssetManager创建的,而该AssetManager查找文件时会从apk中查找,所以新Resources对象也会从apk中查找文件。
范例2:加载已安装apk中的资源。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView img = (ImageView) findViewById(R.id.img);
String pkgName = "com.cutler.androidtest2";
try {
// 获取指定App的Context对象,如果该App未安装,则会抛异常。
Context context = createPackageContext(pkgName,
Context.CONTEXT_IGNORE_SECURITY | Context.CONTEXT_INCLUDE_CODE);
Resources pluginRes = context.getResources();
// 加载文本
System.out.println(pluginRes.getString(
pluginRes.getIdentifier("text","string", pkgName)));
// 加载图片
img.setImageDrawable(pluginRes.getDrawable(
pluginRes.getIdentifier("mv","mipmap", pkgName)));
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
}
语句解释:
- 从上面的代码可以看出来,如果App已经安装在手机上了,则我们可以很容易加载它的资源。
第二节 插件化开源库
通过上面的学习可以发现,动态加载代码和资源的过程其实十分的简单;但是实际开发中的情况要复杂的多,可能会要求我们能动态加载四大组件,这就会有很多问题:
- Activity由代码和布局组成,布局如何加载?
- 由于是我们自己实例化的Activity,它的生命周期方法如何调用?so文件怎么加载?
- 如何保证在各种机型、各版本系统上都正常运行不报错?
- 插如何管理、如何升级?
以上这些问题都需要去解决,如果只靠一个人的话就很慢了,所以笔者更推荐使用开源的插件库。
目前国内开源的较成熟的插件方案有DL
和DroidPlugin
:
- DL方案仅仅对Framework的表层做了处理,严重依赖that语法,编写插件代码和主程序代码需单独区分。
- DroidPlugin通过Hook增强了Framework层的很多系统服务,开发插件就跟开发独立app差不多。
以Activity生命周期的管理为例:
- DL的代理方式就像是牵线木偶,插件只不过是操纵傀儡而已。
- 而DroidPlugin则是借尸还魂,插件就是一个正常的APK,它自己并不知道自己是插件。
- DroidPlugin Hook了系统几乎所有的Sevice,欺骗了大部分的系统API;
- 掌握这个Hook过程需要掌握很多系统原理,因此学习DroidPlugin对于整个Android FrameWork层大有裨益。
最后,笔者有两点要说:
- 首先,虽然笔者推荐去使用开源库,但不代表我们不需要去了解开源库是如何工作的。所以下一节会在源码层次来介绍这两个库的实现原理。
- 然后,就目前而言,笔者更偏向于使用DroidPlugin,上面介绍它的优点不太直观,稍后大家就会明白它的过人之处。
DL
DL
全称 dynamic-load-apk ,是由 singwhatiwanna 发起的一个插件化开源库。
实现原理
现有的问题:
- 所谓的插件化,其实包括“宿主”和“插件”两部分,插件可以是从网上下载到本地的。
- 由于插件未安装到设备上的,所以当我们过反射实例化插件里的Activity时,创建出来的只是一个普通的对象,它是没有Context对象的,这意味着如果我们调用这个Activity的getResources等方法,就会抛异常。
- 也就是说,以前Activity的Context对象是系统设置给它的,而我们自己实例化Activity的话,它就没有Context对象了。
- 我们都知道,Context对象可以做很多事,当Activity没有Context对象时,可以说是寸步难行了,甚至连布局都没法设置。
DL
是这么解决问题的:
- 首先,在宿主项目中定义一个ProxyActivity类,它是正经的Activity。
- 然后,每当宿主项目需要启动插件里的ActivityA时,DL框架都会先启动自己的ProxyActivity。
- 接着,再在ProxyActivity中反射并实例化ActivityA的对象,接着把ProxyActivity的引用传递给ActivityA对象。
- 最后,当ActivityA需要使用Context的时候,就使用ProxyActivity的Context对象。
整个过程说起来简单,实际操作的时候会遇到各种问题,我们接下来就仿写一下这个过程。
范例1:在宿主项目中,创建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
31public class ProxyActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String pluginPath = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/app-debug.apk";
ClassLoader classLoader = new DexClassLoader(
new File(pluginPath).getAbsolutePath(),
getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, getClassLoader());
try {
// 创建插件项目中的MainActivity的实例。
Class<?> clazz = classLoader.loadClass("com.cutler.androidtest2.MainActivity");
Constructor constructor = clazz.getConstructor();
Activity pluginActivity = (Activity) constructor.newInstance();
// 将ProxyActivity设置到pluginActivity中。
Method m1 = clazz.getMethod("setRemoteProxyActivity", Activity.class);
m1.invoke(pluginActivity, this);
// 调用pluginActivity的onCreate方法,并把插件的本地路径传递过去。
Method m2 = clazz.getDeclaredMethod("onCreate", Bundle.class);
m2.setAccessible(true);
Bundle bundle = new Bundle();
bundle.putString("pluginPath", pluginPath);
m2.invoke(pluginActivity, bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
}
语句解释:
- 宿主项目与插件项目不能包含相同的类,否则会抛异常:
- Class ref in pre-verified class resolved to unexpected implementation
- 比如宿主项目引用了suport-v7库,那么插件项目就不能在编译的时候,也把suport-v7库放入apk中。
范例2:插件项目中的MainActivity
。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
62public class MainActivity extends Activity {
// 默认情况下,让它指向自己,这么做是为了可以单独运行插件。
private Activity remoteProxyActivity = this;
protected void onCreate(Bundle bundle) {
Resources resources = null;
if (remoteProxyActivity == this) {
super.onCreate(bundle);
resources = getResources();
} else {
// 程序如果走此流程,则意味着当前Activity是被人当插件使用的。
// 所以不会调用super.onCreate()方法,因为当前Activity只是一个傀儡,它并不是真的Activity。
// 读取宿主的ProxyActivity传递过来的,当前插件的路径,并为当前插件新建一个Resources对象。
resources = getPluginResource(bundle.getString("pluginPath"));
}
// 为了统一处理,这里通过Resources加载一个XmlPullParser,在从XmlPullParser对象中创建布局。
// 之所以不使用LayoutInflater.from(remoteProxyActivity).inflate(int, ViewGroup)加载布局
// 是因为在该方法内部会使用remoteProxyActivity的Resources对象去加载资源,也就是宿主的Resources对象,
// 但是这个对象是无法加载插件中的资源的。
XmlPullParser parser = resources.getLayout(R.layout.activity_main);
View contentView = LayoutInflater.from(remoteProxyActivity).inflate(parser, null);
// 将加载好的布局设置到宿主的ProxyActivity中。
// 到这里大家应该是明白了,上面为什么说DL的实现机制是“操纵傀儡”了吧。
// 即在宿主里开启真正的Activity,而该Activity的布局、生命周期处理都是由插件来处理。
// 但插件仅仅是处理,它处理的结果需要放到宿主的那个Activity上展现。
remoteProxyActivity.setContentView(contentView);
// 为布局中的控件初始化数据。
// 需要注意的是,不要让布局文件中的控件引用任何资源,比如让TextView引用string资源等。
// 而应该在代码里,使用插件的Resources对象,来动态为它们设置值。
ImageView imageView = (ImageView) contentView.findViewById(R.id.image);
imageView.setImageDrawable(resources.getDrawable(R.mipmap.mv));
Button button = (Button) contentView.findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Toast.makeText(remoteProxyActivity,"click from plugin", Toast.LENGTH_SHORT).show();
}
});
}
public Resources getPluginResource(String pluginPath) {
Resources pluginRes = null;
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, pluginPath);
Resources superRes = remoteProxyActivity.getResources();
pluginRes = new Resources(assetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return pluginRes;
}
public void setRemoteProxyActivity(Activity activity) {
this.remoteProxyActivity = activity;
}
}
语句解释:
- 注释写的很清楚了,也是不多说。
以上就模仿了DL
框架的实现原理,接下来我们就跟着程序的执行流程,来阅读源码。
源码阅读
很显然,插件化开发的第一步就是,让宿主项目在运行时把插件项目加载到内存中。
而在DL
中,宿主项目可以使用DLPluginManager
类的loadApk
方法来完成加载任务,我们来看一下源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public DLPluginPackage loadApk(String dexPath) {
return loadApk(dexPath, true);
}
public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
mFrom = DLConstants.FROM_EXTERNAL;
// 读取插件apk的信息,如果插件apk不存在,则返回null。
PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
if (packageInfo == null) {
return null;
}
// 初始化插件apk相关的参数(ClassLoader、Resources、AssetManager对象)。
// 如果已经初始化过了,则不会重复初始化。
DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
// 如果需要,则拷贝so文件,若so已经拷贝过了(依靠最后修改时间来判断)则不会重复拷贝。
if (hasSoLib) {
copySoLib(dexPath);
}
return pluginPackage;
}
语句解释:
- 至于preparePluginEnv方法内部的代码是什么样的,请自行去查看源码。
当加载完插件之后,就可以调用DLPluginManager
类的startPluginActivity
方法来启动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
26
27
28
29
30
31
32
33
34public int startPluginActivity(Context context, DLIntent dlIntent) {
return startPluginActivityForResult(context, dlIntent, -1);
}
public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
// 此处省略若干代码,主要是做安全性校验,比如未调用loadApk初始化插件就调用此方法启动Activity。
// 获取ProxyActivity,若获取失败则返回。
Class<? extends Activity> activityClass = getProxyActivityClass(clazz);
if (activityClass == null) {
return START_RESULT_TYPE_ERROR;
}
// 将要启动的插件里的Activity放到参数里,然后去启动宿主项目中的ProxyActivity。
dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
dlIntent.setClass(mContext, activityClass);
performStartActivityForResult(context, dlIntent, requestCode);
return START_RESULT_SUCCESS;
}
private Class<? extends Activity> getProxyActivityClass(Class<?> clazz) {
Class<? extends Activity> activityClass = null;
// 如果待启动的插件中的类是DLBasePluginActivity的子类,则宿主项目就启动DLProxyActivity类。
// 如果是DLBasePluginFragmentActivity的子类,则宿主就启动DLProxyFragmentActivity。
// 言外之意就是,插件中的所有Activity都必须继承这两个类,否则是无法被启动的。
if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {
activityClass = DLProxyActivity.class;
} else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {
activityClass = DLProxyFragmentActivity.class;
}
return activityClass;
}
语句解释:
- 另外,Class类的isAssignableFrom方法用来比较两个Class对象,而instanceof关键字是判断一个对象是否属于某个类。
接着,我们去查看DLProxyActivity
类的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class DLProxyActivity extends Activity implements DLAttachable {
// 插件Activity的引用。
// 刚才说了插件中的Activity必须继承DLBasePluginActivity和DLBasePluginFragmentActivity二者之一。
// 而它们二者又都实现了DLPlugin接口,所以这个mRemoteActivity其实就是一个Activity对象。
protected DLPlugin mRemoteActivity;
// 用来连接DLProxyActivity和mRemoteActivity类的一个业务类对象。
private DLProxyImpl impl = new DLProxyImpl(this);
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 当DLProxyActivity被启动的时候,调用业务类对象的onCreate方法。
impl.onCreate(getIntent());
}
public void attach(DLPlugin remoteActivity, DLPluginManager pluginManager) {
mRemoteActivity = remoteActivity;
}
// 此处省略若干代码
// 主要都是一些生命周期方法,DLProxyActivity会调用mRemoteActivity去处理。
}
语句解释:
- 这里需要注意的是DLProxyActivity类实现了DLAttachable接口,并重写了接口中的attach方法,稍后会用到。
接着,我们去查看DLProxyImpl
类的源码: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
39public void onCreate(Intent intent) {
intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);
mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
Log.d(TAG, "mClass=" + mClass + " mPackageName=" + mPackageName);
mPluginManager = DLPluginManager.getInstance(mProxyActivity);
mPluginPackage = mPluginManager.getPackage(mPackageName);
mAssetManager = mPluginPackage.assetManager;
mResources = mPluginPackage.resources;
initializeActivityInfo();
handleActivityInfo();
// 启动插件Activity。
launchTargetActivity();
}
protected void launchTargetActivity() {
try {
// 创建插件Activity的对象
Class<?> localClass = getClassLoader().loadClass(mClass);
Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
mPluginActivity = (DLPlugin) instance;
// 将插件Activity的引用设置到代理Activity中
((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
// 将代理Activity的引用也设置到插件Activity中
mPluginActivity.attach(mProxyActivity, mPluginPackage);
// 手工调用插件Activity的onCreate方法
Bundle bundle = new Bundle();
bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
mPluginActivity.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
至此程序的流程就走到了插件中了,由于插件Activity需要继承DLBasePluginActivity
类,所接着来看它的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public void attach(Activity proxyActivity, DLPluginPackage pluginPackage) {
Log.d(TAG, "attach: proxyActivity= " + proxyActivity);
mProxyActivity = (Activity) proxyActivity;
that = mProxyActivity;
mPluginPackage = pluginPackage;
}
public void onCreate(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mFrom = savedInstanceState.getInt(DLConstants.FROM, DLConstants.FROM_INTERNAL);
}
if (mFrom == DLConstants.FROM_INTERNAL) {
super.onCreate(savedInstanceState);
mProxyActivity = this;
that = mProxyActivity;
}
mPluginManager = DLPluginManager.getInstance(that);
Log.d(TAG, "onCreate: from= "
+ (mFrom == DLConstants.FROM_INTERNAL ? "DLConstants.FROM_INTERNAL" : "FROM_EXTERNAL"));
}
语句解释:
- 其中mFrom用来区别插件Activity当前是被宿主加载的还是自己启动的,这么做是为了在开发插件的时候可以调试。
通过以上源码阅读可以知道,DL框架的实现方式就是我们前面说的那样,由于篇幅有限,就不继续深入介绍了。
以上是简单的介绍了DL
框架的基本原理,而DL
里面所做的事情要多得多:
- 支持Service
- 支持在插件中用R访问plugin资源
- 支持so加载、生命周期处理、插件管理等等
使用步骤
在使用DL
进行插件化开发之前,需要先将DL
的源码打包成一个jar
,然后再将它分别引入到宿主
和插件
项目中。
- 也就是说,宿主项目和插件项目都需要引用jar。
- 但是,它们也都分别只会使用到这个jar包中的某几个类,而并不是全部的类。
接下来简单的说一下DL
的使用步骤。
第一步,前往 dynamic-load-apk 将DL
库的源代码下载下来,并导入到Android Studio
中。
- 下载完毕后,先选中“lib”模块,然后打开Android Studio的Build菜单并执行Build Project。
- 完成后,AS会在lib/build/intermediates/bundles/debug(这个目录以后版本可能会变)里生成一个classes.jar。
- classes.jar就是我们稍后会用到的DL库,把它拷贝出来并改名为dl.jar即可。
第二步,创建一个名为DLHost
的宿主项目,并将dl.jar
放入其libs
目录下,并将下面代码放到清单文件中。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<activity
android:name="com.ryg.dynamicload.DLProxyActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="com.ryg.dynamicload.proxy.activity.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name="com.ryg.dynamicload.DLProxyFragmentActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="com.ryg.dynamicload.proxy.fragmentactivity.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<service android:name="com.ryg.dynamicload.DLProxyService" />
语句解释:
- 就像之前说的,DL是通过代理的方式实现的插件化,所以我们需要在宿主项目中配置代理Activity、Service。
第三步,在DLHost
中加载插件。1
2
3
4
5
6
7
8
9
10public void onClick(View view) {
// 加载插件
DLPluginPackage pluginPackage = DLPluginManager.getInstance(this)
.loadApk(Environment.getExternalStorageDirectory() + File.separator + "plugin.apk");
// 如果加载成功,则启动其内的Activity
if(pluginPackage != null) {
DLIntent intent = new DLIntent("com.cutler.dlplugin","com.cutler.dlplugin.MainActivity");
DLPluginManager.getInstance(this).startPluginActivity(this, intent);
}
}
语句解释:
- 本范例用来加载SD卡根目录下的“plugin.apk”。
第四步,创建一个名为DLPlugin
的插件项目,并将dl.jar
放入其libs
目录下,同时修改它的依赖。1
2
3dependencies {
provided fileTree(dir: 'libs', include: ['*.jar'])
}
语句解释:
- 需要注意的是,本范例使用的是“provided”关键字,它表示当前项目在打包的时候,不会把libs目录下的jar给放入APK中。
- 这么做的目的是防止插件和宿主项目引用重复的jar包,因为dl.jar已经在宿主项目中存在了。
- 使用provided关键字后,如果直接安装插件项目的apk的话,运行时就会抛异常,因为系统找不到apk所需要的类。
- 但是如果是在宿主项目动态加载插件项目的话,就不会有问题,因为宿主项目已经把dl.jar加载到进程中了。
第五步,让DLPlugin
的MainActivity
继承DLBasePluginActivity
。1
2
3
4
5
6
7public class MainActivity extends DLBasePluginActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
语句解释:
- 继承之后,就可以让宿主项目加载MainActivity了。
- 需要注意的是,由于插件中的MainActivity并不是真正的启动,所以不要在它里面使用this关键字,而应该使用DL为我们提供的that关键字。
- 如果你不知道that关键字是什么,那请自行阅读DL的源码,笔者只能帮你到这了。
本节参考阅读:
DroidPlugin
在正式介绍DroidPlugin
之前,我们得学一下代理相关的知识。
本节介绍的知识主要参考自 《Android插件化原理解析》 ,推荐大家去阅读该系列文章。
- 不过,如果你以为只看他写的博客就能日天的话,呵呵,那你就错了。
- 他写的知识我会写,他没写的我也会写,所以看我写的,才能日天! 哥就是抄,也能抄出自己的风格!
在继续向下之前,请各位先去《架构篇 第一章 设计模式》看一下代理设计模式,因为接下来要介绍的Hook就是基于代理模式的。
Hook
我们把通过“代理”
技术来替换掉对象,并修改参数,替换返回值等行为,称之为Hook
。
- 具体来说,Hook是基于Java的反射技术来实现的。
现在我们有一个任务:
- Hook掉startActivity方法,使得在任何地方调用这个方法时,都能额外弹出一条Toast。
Hook的思路为:
- 首先,Hook操作的第一步就是寻找Hook点,即找到一个可以被我们替换的对象。
- 查看startActivity的源码,发现最终会调用Activity的mInstrumentation属性的execStartActivity方法。这意味着,若能把Activity的mInstrumentation属性替换为我们的对象,那么启动Activity时调用的就是我们的对象了。
- 第二,通过代码搜索得知Activity的mInstrumentation属性是在它的attach方法中初始化的。
- 第三,我们知道Activity的attch方法是由ActivityThread的performLaunchActivity方法调用的(你现在知道了),因此继续看ActivityThread中的Instrumentation对象是哪来的。
- 第四,通过阅读android-23版本的ActivityThread源码发现,Instrumentation对象是它的一个属性。
- 这意味着,我们如果把ActivityThread的Instrumentation属性给Hook掉,那么就能完成任务了。
- 同时也发现ActivityThread类的currentActivityThread方法可以获取它的对象。
有了思路后,接下来就开始Hook吧。
范例1:在Application里执行Hook操作。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
32public class MyApplication extends Application {
public void onCreate() {
super.onCreate();
try {
// 由于ActivityThread类被hide了,所以只能通过反射来获取它的Class对象。
Class clazz = Class.forName("android.app.ActivityThread");
// 下面这段代码用来获取ActivityThread对象。
// 需要注意的是,不同版本的Android源码的ActivityThread的内部实现是不同的。
// 所以为了确保反射成功,我们应该调用currentActivityThread方法来获取ActivityThread的对象。
// 而不是直接访问ActivityThread的sCurrentActivityThread属性。
// 简单的说,在android2.x的源码里,ActivityThread是没有sCurrentActivityThread属性的。
// 它是用过ThreadLocal来保存变量。
Method currentActivityThreadMethod = clazz.getMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取mInstrumentation属性
Field mInstrumentationField = clazz.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
Instrumentation instrumentation = (Instrumentation)
mInstrumentationField.get(currentActivityThread);
// 使用我们自定义的CutlerInstrumentation,替换mInstrumentation属性
mInstrumentationField.set(currentActivityThread, new CutlerInstrumentation(instrumentation));
System.out.println(instrumentation);
} catch (Exception e) {
e.printStackTrace();
}
}
}
语句解释:
- 之所以把Hook的代码写在Application里是想在程序启动的第一时间执行Hook操作。
范例2:CutlerInstrumentation类。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// 首先让我们的类继承Instrumentation,不然没法用它进行替换。
public class CutlerInstrumentation extends Instrumentation {
// 还得持有被Hook的对象,因为启动Activity的操作,还是得由它来完成。
Instrumentation obj;
public CutlerInstrumentation(Instrumentation obj) {
this.obj = obj;
}
// 定义一个与父类具有相同签名的方法。
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Toast.makeText(who, "Cutler 虎爷! 到此一游!", Toast.LENGTH_SHORT).show();
ActivityResult result = null;
try {
// 调用obj同名的方法,执行启动Activity的任务。
// 由于Instrumentation类的execStartActivity方法也被hide,所以只能通过反射进行调用。
Method execStartActivityMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivityMethod.setAccessible(true);
result = (ActivityResult) execStartActivityMethod.invoke(obj, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
语句解释:
- 然后运行程序,在启动Activity时就可以看到Toast消息了。
需要注意的是:
- 不同的Android版本的源码是不同的,如果你把上面的代码运行在2.3.2(及以下)的手机上,就会发现并没有Toast弹出来。
这是因为在 2.3.2版本中的Activity的源码 调用的是:
- execStartActivity(Context, IBinder, IBinder, Activity,Intent, int)
而范例2
重写的方法比它多一个Bundle
参数,所以只要在CutlerInstrumentation
类中也重写该方法即可。
还有一点需要注意的是:
- Hook的作用于仅限于当前进程。
- 也就是说,如果你在A进程中hook了ActivityThread,那么B进程中调用的startActivity方法时,并不会弹出Toast,即便B进程和A进程是属于同一个项目的两个不同的进程也不行。
- 这很好理解,即ActivityThread等Framework层的API是运行在我们进程中的,它们与远程的系统进程中的服务进行IPC通信。无论我们对自己进程中的API做何种修改,都不会影响系统进程中服务的执行流程。
DroidPlugin简介
DroidPlugin
全称 Qihoo360/DroidPlugin ,是由 Andy Zhang 发起的一个插件化开源库。
我们首先要明白的一点是:
- 正常情况下,任何人都无法调起一个未安装的apk中的Activity。
- 市面上的各类插件化框架都是通过“代理”的方式成功加载插件中的Activity的,不同只是它们的实现代理方式。
一个成熟的插件框架通常具备如下特点:
- 使用DexLoader动态的加载dex文件进入进程中。
- 在宿主的AndroidManifest.xml中预注册一些将要使用的四大组件,做为代理组件。
- 能处理好插件中的so文件、Resources对象、保证R文件的正确使用。
笔者引用知乎上两位答主的答案:
@周柯文 - 原文连接
360这个就是用的动态代理,而且用的非常彻底,看的出来开发者很熟悉AOP和Android。
DroidPlugin把所有常用到的XXXManager都代理了一遍,然后由自己模拟的各种XXXManagerHookHandle接管,并且开发者为版本兼容性做了很大的努力。
总之是个很值得学习的框架。
@AndyZhang - 原文连接
1、基于动态代理的Hook,我们通过此,hook了系统的大部分与system—server进程通讯的函数,以此作为“欺上瞒下”的目的,欺骗系统“以为”只有一个apk在运行,瞒过插件让其“认为”自己已经安装。
2、基于Android的多个apk可以运行在同一个进程的原理。
3、预注册 Activity等组件实现免注册。
4、灵活的进程管理,回收机制。
接下来,笔者将从源码角度来介绍DP
的工作机制,但是不会介绍的太深(因为需要的前驱知识太多)。
DP的启动流程
官方文档要求我们在Application
创建的时候,执行如下代码:1
2
3
4
5
6
7
8
9
10
11
12
public void onCreate() {
super.onCreate();
//这里必须在super.onCreate方法之后,顺序不能变
PluginHelper.getInstance().applicationOnCreate(getBaseContext());
}
protected void attachBaseContext(Context base) {
PluginHelper.getInstance().applicationAttachBaseContext(base);
super.attachBaseContext(base);
}
那我们就从PluginHelper
类的applicationOnCreate
方法开始吧。
通过阅读源码发现,applicationOnCreate
方法会转调用initPlugin
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14private void initPlugin(Context baseContext) {
// 此处省略若干代码
// 安装各个Hook类
PluginProcessManager.installHook(baseContext);
// 此处省略若干代码
// 将已经安装到DP中的插件加载到内存中
PluginManager.getInstance().addServiceConnection(PluginHelper.this);
PluginManager.getInstance().init(baseContext);
// 此处省略若干代码
}
语句解释:
- 在DP中,对于插件有两个操作:安装、卸载。
- 当我们把SD卡中的插件安装到DP中后,DP会将其解压到宿主项目中的/data/data目录下,这意味着如果随后用户把SD卡上的文件给删除了,DP依然能访问到插件。
- 卸载就不用说了。
接着调到了HookFactory
类的installHook
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public final void installHook(Context context, ClassLoader classLoader) throws Throwable {
installHook(new IClipboardBinderHook(context), classLoader);
// 此处省略若干代码
installHook(new IActivityManagerHook(context), classLoader);
installHook(new InstrumentationHook(context), classLoader);
// 此处省略若干代码
}
public void installHook(Hook hook, ClassLoader cl) {
try {
// 调用Hook类的onInstall方法执行安装操作。
hook.onInstall(cl);
synchronized (mHookList) {
mHookList.add(hook);
}
} catch (Throwable throwable) {
Log.e(TAG, "installHook %s error", throwable, hook);
}
}
语句解释:
- 可以看出来,在DP被初始化的时候,就会安装很多Hook到系统中,但是这些Hook只会在当前进程中有效。
- 正如你所见到的那样,在DP中Hook类是所有XxxHook的父类。
我们以InstrumentationHook
类为例,看一下它的源码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class InstrumentationHook extends Hook {
// 此处省略若干代码
protected void onInstall(ClassLoader classLoader) throws Throwable {
// 获取ActivityThread.mInstrumentation属性
Object target = ActivityThreadCompat.currentActivityThread();
Class ActivityThreadClass = ActivityThreadCompat.activityThreadClass();
Field mInstrumentationField = FieldUtils.getField(ActivityThreadClass, "mInstrumentation");
Instrumentation mInstrumentation = (Instrumentation)
FieldUtils.readField(mInstrumentationField, target);
// 创建我们自己的PluginInstrumentation,并用它替换掉之前的Instrumentation。
if (!PluginInstrumentation.class.isInstance(mInstrumentation)) {
PluginInstrumentation pit = new PluginInstrumentation(mHostContext, mInstrumentation);
pit.setEnable(isEnable());
mPluginInstrumentations.add(pit);
FieldUtils.writeField(mInstrumentationField, target, pit);
}
// 此处省略若干代码
}
}
语句解释:
- 从上面的代码可以看出,DP框架Hook的步骤和我们之前说的步骤是一样的。
- 也就是说,当程序执行startActivity时,系统会调用PluginInstrumentation去处理。
- 但是如果你打开PluginInstrumentation类看时会发现,它根本就没有重写execStartActivity方法,这是为什么呢?
- 这是因为之前笔者为了方便讲解,才对Instrumentation进行Hook的。
- 其实更适合Hook的对象是ActivityManagerNative类,只是怕大家迷糊才没对它Hook。
我们如果点开Instrumentation
类的execStartActivity
方法可以看到: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
32public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
// 此处省略若干代码
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
// 检测是否成功启动了Activity
checkStartActivityResult(result, intent);
// 此处省略若干代码
}
public static void checkStartActivityResult(int res, Object intent) {
// 此处省略若干代码
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
// 此处省略若干代码
}
}
语句解释:
- 发现Instrumentation类其实又调用了ActivityManagerNative类的startActivity方法。
- 同时我们也在这里看到了那个常见的异常:
- have you declared this activity in your AndroidManifest.xml?
事实上我们前面看到的IActivityManagerHook
类就是ActivityManagerNative
类的Hook
类,它的源码: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
35public class IActivityManagerHook extends ProxyHook {
// 此处省略若干代码
public BaseHookHandle createHookHandle() {
return new IActivityManagerHookHandle(mHostContext);
}
public void onInstall(ClassLoader classLoader) throws Throwable {
// 先获取ActivityManagerNative类的静态属性gDefault。
Class cls = ActivityManagerNativeCompat.Class();
Object obj = FieldUtils.readStaticField(cls, "gDefault");
if (obj == null) {
ActivityManagerNativeCompat.getDefault();
obj = FieldUtils.readStaticField(cls, "gDefault");
}
if (IActivityManagerCompat.isIActivityManager(obj)) {
setOldObj(obj);
Class<?> objClass = mOldObj.getClass();
// 此处省略若干代码
// 使用动态代理创建一个代理对象
Object proxiedActivityManager = MyProxy.newProxyInstance(objClass.getClassLoader(), ifs, this);
// 将我们的代理对象设置到ActivityManagerNative类的gDefault属性上。
FieldUtils.writeStaticField(cls, "gDefault", proxiedActivityManager);
}
// 此处省略若干代码
}
}
语句解释:
- 简而言之,此后我们不论在什么地方调用ActivityManagerNative的方法,最终都会被IActivityManagerHook接管。
- 也就说会调用IActivityManagerHook的invoke方法,该方法定义在其父类ProxyHook中。
由于篇幅有限,后续的操作就不贴代码了,下面简单说一下后续步骤:
- 在ProxyHook类的invoke方法中会调用mHookHandles.getHookedMethodHandler去处理。
- 其中mHookHandles是在Hook类的createHookHandle方法中被初始化的。
- 换到IActivityManagerHook类的话,它的mHookHandles属性就是IActivityManagerHookHandle类型的。
- 接着打开IActivityManagerHookHandle类,看他的init方法就能明白了,它拦截了哪些方法。
继续深入的话,大家很容易迷失在代码里,所以如果真想看的话自己去看就行,总之:
- 当我们调用startActivity方法时,IActivityManagerHookHandle.startActivity类的beforeInvoke方法会被调用。而在该方法中会修改Intent的内容,即让系统去启动代理Activity,同时该方法也处理了各个系统版本的兼容性问题。
DP的使用
使用方法官方文档已经介绍的很清楚了,有几个需要注意的问题是:
- 截止至2016.3.25日,DP的项目源码还是Eclipse格式的,不过笔者推荐大家在AndroidStudio中使用DP。
- 将DP导入到AndroidStudio中后,如果发现AIDL文件无法生成,可以clean、build一下项目。
- 正式使用时,只需要将Libraries-DroidPlugin导入即可。
本节参考阅读:
第三节 结尾
除了上一节中所介绍的插件库之外,市面上还有不少优秀的插件库,由于精力关系笔者就不再一一介绍了。
笔者选择插件库时,会主要考虑如下几点:
- 成熟稳定。即经历了大量的用户测试、处理了各种兼容问题。 这一点DL和DP都符合。
- 易移植。 即API入侵低,可以方便的从该插件库移植到另一个插件库。这一点DP要更胜一筹。
- 代码性能。当然是越快越好、越少占内存越好。这一点DL要更胜一筹,毕竟DP里有很多反射和静态属性。
- 功能完备(可选)。即插件库提供了除“安装、卸载”以外的其他功能。
综合考虑的话,笔者暂时更倾向于使用DP
,但不排除以后随着笔者对它们二者的了解加深,情况会有所改变。