一、Android插件化技术
我们在平时的开发过程中,会经常遇到产品需求的变更或者出现bug,在传统的模式中,我们需要首先需要修改代码,然后重新打包Apk,再交给公司的运营去官网或者应用商店上线,用户在打开应用的时候就会进行更新了。但是这种模式有几个缺点,一是上线周期长,从修改代码到用户更新需要较长的时间;二是用户更新代价较大,每次用户更新都需要下载整个Apk包,整个Apk包包括了一个应用的所有代码,要消耗用户较多的流量,并且,如果是一些重要的更新,为了确保用户都能更新到,还需要用到强制更新,即用户打开App后如果不更新应用则退出应用,这种对用户来说是极其不友好的。还有另外一种情况,某些较大的App功能很多,比如支付宝、微信等,如果将这些功能全部塞到一个Apk中,那将会是一个巨型Apk,用户在安装或者更新Apk时将会经过漫长的等待时间。基于以上两点,Android的插件化技术应运而生,插件化技术即将Apk按照功能模块划分,不同的功能打包成不同的Apk,然后应用的主Apk按需加载对应功能的Apk,用户只需要安装应用的主Apk即可,主Apk相当于一个壳,它会按需加载其他功能模块的Apk。通过这种模式,不仅解决了巨型Apk的问题,而且当某个功能模块需要变化时,也只需要修改对应功能的代码,打包功能Apk并更新即可,这样不仅可以让用户及时更新,而且更新的代价也很小。但是,我们知道,在Android中,没有安装的apk是不能直接运行的,那么要想实现插件化,我们就必须能够让主Apk能够加载功能Apk并运行。在这篇文章中,我们就一步步的分析如何实现Android的插件化。
二、需求分析
需求:实现在一个Apk中加载另外一个Apk并运行。
一般来说,Apk只有安装了才能够运行,Android在安装Apk时会解析Apk包,解析其中的AndroidManifest.xml文件,我们的四大组件(除了动态注册的广播)等都配置在其中,解析过程是由PackageManagerService完成的,PMS在解析Apk完成后,会将声明的四大组件的信息都注册在其中,并且应用在获取Apk资源时,也是需要通过PMS的协助完成,我们的应用可以通过上下文环境Context来和PMS打交道,对于一个没有安装过的Apk,其配置信息是没有在PMS中注册的,那么通过宿主Apk的上下文环境是无法去获取功能Apk中的四大组件以及资源等信息的,也就无法运行功能Apk。我们知道,PMS和AMS都运行在System Server进程中,我们的宿主Apk是运行在自己的应用进程中,不同的进程直接的数据是隔离的,我们无法在自己的应用进程中直接操作到运行在System Server进程中的PMS和AMS,两者之间的通信过程是通过Binder机制来完成的,也就是说,要想在宿主Apk中运行功能Apk的四大组件,就需要欺骗运行在System Server中的PMS和AMS,让其误以为我们的宿主Apk运行的是自己应用中的组件。基于以上思想,我们可以想到以下思路,首先来看一下针对Activity的处理:
1、在宿主Apk中注册一个代理的Activity,暂定为ProxyActivity;
2、当我们在宿主应用中要加载功能Apk时,首先要解析功能Apk,将其加载到对应的ClassLoader中;
3、启动ProxyActivity,因为ProxyActivity是在宿主Apk中注册过的,所有可以启动;
4、从功能Apk的ClassLoader中找到要功能Activity类;
5、通过反射创建功能Activity实例;
6、在ProxyActivity的所有生命周期回调函数中都调用功能Activity对应的回调函数,这样功能Activity要完成的功能就都在ProxyActivity中完成了。
可以看到,这是一种典型的代理模式,通过启动ProxyActivity来欺骗AMS,让AMS认为我们要启动的Activity是宿主Apk中的Activity,然而实际上ProxyActivity只是一个壳,它的主要作用是用来回调功能Activity的生命周期函数,这样就通过ProxyActivity完成了功能Activity的功能。前面说到过,没有安装过的Apk是没有在PMS中注册的,我们通过在宿主应用的Context是无法访问到功能Activity的资源的,这里我们暂时不讨论资源的加载问题,先只看如何通过ProxyActivity运行到功能Activity的代码。
如何动态加载Apk
首先,我们要知道如何将功能Apk加载到对应的ClassLoader中,关于动态加载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是不行的,必须转换成Android虚拟机所能识别的字节码文件,通过以上介绍,我们需要用DexClassLoader来加载功能Apk。
代码
首先我们在宿主应用中新建ProxyActivity,代码如下:
package com.liunian.androidbasic.dynamicload;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class ProxyActivity extends Activity {
public static final String PLUGIN_DEX_PATH = "plugin.dex.path";
public static final String PLUGIN_ACTIIVTY_CLASS_NAME = "plugin.activity.class.name";
Activity mPluginActivity = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null) {
// 从Intent中获得要启动的功能Apk的路径和Activity完整类名
String pluginDexPath = intent.getStringExtra(PLUGIN_DEX_PATH);
String pluginActivityClassName = intent.getStringExtra(PLUGIN_ACTIIVTY_CLASS_NAME);
if (TextUtils.isEmpty(pluginDexPath) || TextUtils.isEmpty(pluginActivityClassName)) {
return;
}
// 根据apk路径加载apk代码到DexClassLoader中
File dexOutputDir = this.getDir("dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(pluginDexPath,
dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
if (dexClassLoader == null) {
return;
}
// 从DexClassLoader中获得功能Activity Class对象并通过反射创建一个功能Activity实例
Class pluginActivityClass = null;
try {
pluginActivityClass = dexClassLoader.loadClass(pluginActivityClassName);
mPluginActivity = (Activity) pluginActivityClass.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
// 调用功能Activity的setProxyActivity方法,给其设置代理Activity
try {
Method setProxyActivityMethod = pluginActivityClass.getDeclaredMethod("setProxyActivity", Activity.class);
setProxyActivityMethod.invoke(mPluginActivity, this);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 调用功能Activity实例的onCreate方法
try {
Method onCreateMethod = Activity.class.getDeclaredMethod("onCreate", Bundle.class);
onCreateMethod.setAccessible(true);
onCreateMethod.invoke(mPluginActivity, savedInstanceState);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
宿主应用中启动功能Apk中的Activity:
Intent intent = new Intent();
intent.putExtra(ProxyActivity.PLUGIN_DEX_PATH, "/sdcard/pluginmodule-debug.apk");
intent.putExtra(ProxyActivity.PLUGIN_ACTIIVTY_CLASS_NAME, "com.alipay.pluginmodule.MainActivity");
intent.setClass(MainActivity.this, ProxyActivity.class); // 其实启动的还是ProxyActivity
在Android studio中新建一个应用工程,MainActivity内容如下:
package com.alipay.pluginmodule;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends Activity {
Activity mProxyActivity = null;
public void setProxyActivity(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("liunianprint:", "MainActivity onCreate");
mProxyActivity.setContentView(R.layout.activity_main);
}
}
编译功能Apk,名称为pluginmodule-debug.apk,将其push到手机的sdcard目录下,在宿主应用中启动它,发现"MainActivity onCreate"有打印:
02-14 15:18:33.944 22742-22742/com.alipay.pluginmodule I/liunianprint:: MainActivity onCreate
但是界面上确没有显示任何内容,我们再MainActivity中通过setContentView给页面设置的布局并没有显示,为什么会这样呢?原因是因为我们通过宿主应用的上下文环境无法加载到在功能Apk中的资源,所以setContentView中设置的布局也就无效了,那么我们让功能Activity显示内容呢?既然无法通过设置布局id的方式给功能Activity添加布局,我们何不尝试一下手动给功能Activity添加控件呢?
public class MainActivity extends Activity {
Activity mProxyActivity = null;
public void setProxyActivity(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("liunianprint:", "MainActivity onCreate");
mProxyActivity.setContentView(generateLayout()); // 给代理Activity设置手动生成的控件布局
}
private View generateLayout() {
TextView textView = new TextView(mProxyActivity);
textView.setText("我是Plugin的MainActivity");
return textView;
}
}
这个时候在从宿主Apk中启动功能Apk中的Activity,显示如下:
可以看到,成功的在宿主Apk中显示了功能Apk的内容,万里长征终于迈出了第一步,总结一下使用到的技术:
1、通过DexClassLoader动态的加载了功能Apk;
2、通过反射我们可以创建功能Activity的实例,并调用其函数;
3、通过注册代理Activity,然后在代理Activity的生命周期函数中调用功能Activity的生命周期函数,以达到让代理Activity实现功能Activity的效果;
4、功能Activity相当于只是一个代码块的封装,而代理Activity是实际启动的Activity,代理Activity可以看做一个壳,它会调用功能Activity的方法来完成功能Activity的效果。
加载Apk中的资源
在上面的代码中,我们通过动态加载技术成功的执行了功能Activity的代码,但是,却不能使用Apk中的资源,原因是因为通过宿主Apk的Context无法访问到功能Apk中的资源,那么,有没有什么办法可以让宿主Apk访问到功能Apk中的资源呢?下面我们就一起探寻一下解决办法。
Android的资源加载过程是一个复杂的过程,后面我们专门写一篇文章来讨论,这里,我们只探究一下如何通过Android系统提供的Api达到加载其他Apk中的资源的目的,首先,我们来看一下Activity的setContentView方法,看其是如何加载布局资源的:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID); // 其实是将布局资源设置给了Activity的Window,Window的具体实现类为PhoneWindow
initWindowDecorActionBar();
}
其实是将布局资源设置给了Activity的Window,Window的具体实现类为PhoneWindow,继续看PhoneWindow的setContentView方法:
@Override
public void setContentView(int layoutResID) {
...
mLayoutInflater.inflate(layoutResID, mContentParent); // 调用了LayoutInflater的inflate方法解析layoutResID对应的布局文件,生成布局树,并将其添加到mContentParent上
...
}
调用了LayoutInflater的inflate方法解析layoutResID对应的布局文件,生成布局树,并将其添加到mContentParent上,mContentParent是PhoneWindow中的根View---DecorView下的一个子控件,这样我们就通过setContentView方法将布局添加到了DecorView的子控件中。我们再来看一下LayoutInflater的inflate方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources(); // 根据上下文环境获得Resources对象
final XmlResourceParser parser = res.getLayout(resource); // 根据Resources对象获得对应的布局文件
try {
return inflate(parser, root, attachToRoot); // 解析布局文件并生成控件树,如有必要,还需要将其添加到root中
} finally {
parser.close();
}
}
可以看到,Android获得资源首先是根据上下文环境获得一个Resources对象,然后在根据这个Resources对象即可获得其中的资源。那么我们如何构造一个加载了未安装Apk的Resource对象呢?我们来看一下Resources类的构造函数:
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
其中,AssetManager 表示资源管理器,Resources内部就是通过它来读取Apk中的资源的,DisplayMetrics表示屏幕分比率,Configuration表示设备配置。我们可以通过调用AssetManager的addAssetPath方法将Apk中的资源目录添加到AssetManager中管理,那么通过Resources去查找资源时,就会查找Apk中的资源。我们可以通过反射来构建一个AssetMananger对象,然后调用其addAssetPath方法将Apk中的资源目录添加到AssetManager中管理,再通过AssetMananger、DisplayMetrics以及Configuration来构建一个Resources对象,这样就可以访问到Resources中的资源了。代码如下:
public class ProxyActivity extends Activity {
public static final String PLUGIN_DEX_PATH = "plugin.dex.path";
public static final String PLUGIN_ACTIIVTY_CLASS_NAME = "plugin.activity.class.name";
Activity mPluginActivity = null;
Resources mPluginResourcs = null;
String mPluginDexPath;
String mPluginActivityClassName;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent != null) {
// 从Intent中获得要启动的功能Apk的路径和Activity完整类名
mPluginDexPath = intent.getStringExtra(PLUGIN_DEX_PATH);
mPluginActivityClassName = intent.getStringExtra(PLUGIN_ACTIIVTY_CLASS_NAME);
if (TextUtils.isEmpty(mPluginDexPath) || TextUtils.isEmpty(mPluginActivityClassName)) {
return;
}
loadApkResources(); // 加载资源
// 根据apk路径加载apk代码到DexClassLoader中
File dexOutputDir = this.getDir("dex", 0);
DexClassLoader dexClassLoader = new DexClassLoader(mPluginDexPath,
dexOutputDir.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
if (dexClassLoader == null) {
return;
}
// 从DexClassLoader中获得功能Activity Class对象并通过反射创建一个功能Activity实例
Class pluginActivityClass = null;
try {
pluginActivityClass = dexClassLoader.loadClass(mPluginActivityClassName);
mPluginActivity = (Activity) pluginActivityClass.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
// 调用功能Activity的setProxyActivity方法,给其设置代理Activity
try {
Method setProxyActivityMethod = pluginActivityClass.getDeclaredMethod("setProxyActivity", Activity.class);
setProxyActivityMethod.invoke(mPluginActivity, this);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 调用功能Activity实例的onCreate方法
try {
Method onCreateMethod = Activity.class.getDeclaredMethod("onCreate", Bundle.class);
onCreateMethod.setAccessible(true);
onCreateMethod.invoke(mPluginActivity, savedInstanceState);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 加载插件Apk的资源
private void loadApkResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance(); // 通过反射创建一个AssetManager对象
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class); // 获得AssetManager对象的addAssetPath方法
addAssetPathMethod.invoke(assetManager, mPluginDexPath); // 调用AssetManager的addAssetPath方法,将apk的资源添加到AssetManager中管理
mPluginResourcs = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration()); // 根据AssetMananger创建一个Resources对象
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
// 重写ProxyActivity的getResources方法,让其返回插件Apk的资源对象
@Override
public Resources getResources() {
if (mPluginResourcs != null) {
return mPluginResourcs;
}
return super.getResources();
}
}
同时,修改插件Apk的代码:
public class MainActivity extends Activity {
Activity mProxyActivity = null;
public void setProxyActivity(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.i("liunianprint:", "MainActivity onCreate");
mProxyActivity.setContentView(R.layout.activity_main);
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="I am plugin's MainActivity!"
android:layout_centerInParent="true"/>
</RelativeLayout>
重新打包插件Apk和宿主Apk,运行程序:
哈哈,我们成功的显示了插件Apk中的布局文件,总结一下使用到的技术:
1、通过反射构造一个资源管理AssetManager对象;
2、通过反射调用AssetManager的addAssetPath方法,将插件Apk中的资源添加到AssetManager对象中管理;
3、通过AssetManager对象、屏幕分辨率以及设置配置信息构造一个Resources对象;
4、重写ProxyActivity的getResources方法,让其返回可以访问插件Apk资源的Resources对象。
三、总结
这篇文章通过代理的方式实现简单的插件化,插件化主要要做到三件事:
1、加载未安装的Apk中的代码;
2、加载未安装的Apk中的资源;
3、欺骗AMS,绕过检测。
其中欺骗AMS,现在一般是使用Hook的方式实现的,Hook的方式需要我们对Activity的启动流程比较了解,可以Hook Ams的代理或者Hook Instrumentation,即欺骗AMS要启动的Activity是已经注册过的Activity,绕过检测,但是在创建Activity时在将其替换成插件中的Activity,这种方式我们在后面专门写一篇文章分析。