以前公司也做过插件化的开发,偶然的一天网上逛书店,看到这本书,买来看看,到现在大概看了几章,感觉这本书差点意思。包含的东西很多,但是感觉里面的东西都不是太深,甚至有些地方个人感觉都是错误的。比如里面contentprovider的本质是把数据存储到数据库里。当然也有很多以前没有接触过的,也是有所收获的,同时也感谢作者的分享。主观感觉,不喜勿喷。欢迎指正。
2012年7月27日,是Android插件化技术的第一个 里程碑。大众点评的屠毅敏(Github名为mmin18), 发布了第一个Android插件化开源项目 AndroidDynamicLoader
2013年,出现了23Code。23Code提供了一个 壳,在这个壳里可以动态下载插件,然后动态运行。 我们可以在壳外编写各种各样的控件,在这个框架下 运行。
2013年3月27日,第16期阿里技术沙龙,淘宝客 户端的伯奎做了一个技术分享,专门讲淘宝的Atlas插 件化框架,包括ActivityThread那几个类的Hook、增 量更新、降级、兼容等技术。这个视频[2],
2014年3月30日8点20分,是Android插件化的第 二个里程碑。任玉刚开源了一个Android插件化项目 dynamic-load-apk
2014年5月 张涛发布了他的第一个插件化框架 CJFrameForAndroid
2014年11月,houkx在GitHub上发布了插件化项 目android-pluginmgr
2015年。高中生Lody此刻还是高二学 生。他是从初中开始研究Android系统源码的。 第一个著名的开源项目是TurboDex
2015年3月底,Lody发布插件化项目Direct-Loadapk
2015年5月,limpoxe发布插件化框架AndroidPlugin-Framework[10]。
2015年7月,kaedea发布插件化框架androiddynamical-loading[11]。
2015年8月27日,是Android插件化技术的第三个 里程碑。张勇的DroidPlugin
2015年10月携程开源了他们的插件化框架 DynamicAPK[13],
2015年12月底,林光亮的Small框架发布
2016年8月,掌阅推出Zeus[14]。
2017年3月,阿里推出Atlas[15]。
2017年6月26日,360手机卫士的RePlugin[16]。
2017年6月29日,滴滴推出VisualApk[17]。
[1] 开源项目地址: https://github.com/mmin18/AndroidDynamicLoader
[2] 视频地址: http://v.youku.com/v_show/id_XNTMzMjYzMzM2.html
[3] 开源项目地址: https://github.com/singwhatiwanna/dynamic-load-apk
[4] 参考文章: https://blog.csdn.net/lostinai/article/details/50496976 47
[5] 张涛的开源实验室:https://kymjs.com
[6] 开源项目地址: https://github.com/kymjs/CJFrameForAndroid
[7] 开源项目地址:https://github.com/houkx/androidpluginmgr
[8] 开源项目地址: https://github.com/asLody/TurboDex
[9] 开源项目地址: http://git.oschina.net/oycocean/Direct-Load-apk
[10] 开源项目地址: https://github.com/limpoxe/Android-Plugin-Framework
[11] 开源项目地址:https://github.com/kaedea/androiddynamical-loading
[12] 田维术的技术博客:http://weishu.me
[13] 开源项目地址: https://github.com/CtripMobile/DynamicAPK
[14] 开源项目地址: https://github.com/iReaderAndroid/ZeusPlugin
[15] 开源项目地址:https://github.com/alibaba/atlas
[16] 开源项目地址: https://github.com/Qihoo360/RePlugin
[17] 开源项目地址: https://github.com/didi/VirtualAPK
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
}
创建mInstrumentation 的子类
public class EvilInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public EvilInstrumentation(Instrumentation base) {
mBase = base;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d(TAG, "XXX到此一游!");
// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
Class[] p1 = {Context.class, IBinder.class,
IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class};
Object[] v1 = {who, contextThread, token, target,
intent, requestCode, options};
return (ActivityResult) RefInvoke.invokeInstanceMethod(
mBase, "execStartActivity", p1, v1);
}
}
通过反射调用
Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(Activity.class, this, "mInstrumentation");
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
RefInvoke.setFieldObject(Activity.class, this, "mInstrumentation", evilInstrumentation);
// 执行正常的页面跳转startActivity()
public class AMSHookHelper {
public static final String EXTRA_TARGET_INTENT = "extra_target_intent";
public static void hookAMN() throws ClassNotFoundException,
NoSuchMethodException, InvocationTargetException,
IllegalAccessException, NoSuchFieldException {
//获取AMN的gDefault单例gDefault,gDefault是final静态的
Object gDefault = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative", "gDefault");
// gDefault是一个 android.util.Singleton对象; 我们取出这个单例里面的mInstance字段
Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", gDefault, "mInstance");
// 创建一个这个对象的代理对象MockClass1, 然后替换这个字段, 让我们的代理对象帮忙干活
Class<?> classB2Interface = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] { classB2Interface },
new MockClass1(mInstance));
//把gDefault的mInstance字段,修改为proxy
RefInvoke.setFieldObject("android.util.Singleton", gDefault, "mInstance", proxy);
}
}
class MockClass1 implements InvocationHandler {
private static final String TAG = "MockClass1";
Object mBase;
public MockClass1(Object base) {
mBase = base;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
Log.e("bao", method.getName());
return method.invoke(mBase, args);
}
return method.invoke(mBase, args);
}
}
在初始化hook逻辑
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
AMSHookHelper.hookAMN();
} catch (Throwable throwable) {
throw new RuntimeException("hook failed", throwable);
}
}
public class HookHelper {
public static void attachBaseContext() throws Exception {
// 先获取到当前的ActivityThread对象
Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");
// 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
Handler mH = (Handler) RefInvoke.getFieldObject(currentActivityThread, "mH");
//把Handler的mCallback字段,替换为new MockClass2(mH)
RefInvoke.setFieldObject(Handler.class, mH, "mCallback", new MockClass2(mH));
}
}
public class MockClass2 implements Handler.Callback {
Handler mBase;
public MockClass2(Handler base) {
mBase = base;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
// ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
// 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
case 100:
handleLaunchActivity(msg);
break;
}
mBase.handleMessage(msg);
return true;
}
private void handleLaunchActivity(Message msg) {
// 这里简单起见,直接取出TargetActivity;
Object obj = msg.obj;
Log.d("baobao", obj.toString());
}
}
初始化
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
try {
// 在这里进行Hook
HookHelper.attachBaseContext();
} catch (Exception e) {
e.printStackTrace();
}
}
public class HookHelper {
public static void attachContext() throws Exception{
// 先获取到当前的ActivityThread对象
Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread");
// 拿到原始的 mInstrumentation字段
Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread, "mInstrumentation");
// 创建代理对象
Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation);
// 偷梁换柱
RefInvoke.setFieldObject(currentActivityThread, "mInstrumentation", evilInstrumentation);
}
}
public class EvilInstrumentation extends Instrumentation {
private static final String TAG = "EvilInstrumentation";
// ActivityThread中原始的对象, 保存起来
Instrumentation mBase;
public EvilInstrumentation(Instrumentation base) {
mBase = base;
}
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
Log.d(TAG, "包建强到此一游!");
return mBase.newActivity(cl, className, intent);
}
public void callActivityOnCreate(Activity activity, Bundle bundle) {
Log.d(TAG, "到此一游!");
// 开始调用原始的方法, 调不调用随你,但是不调用的话, 所有的startActivity都失效了.
// 由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法
Class[] p1 = {Activity.class, Bundle.class};
Object[] v1 = {activity, bundle};
RefInvoke.invokeInstanceMethod(
mBase, "callActivityOnCreate", p1, v1);
}
}
AMS的逻辑涉及整个系统,所以无法hook,也不能hook。
所以hook就从AMS的入口和出口进行hook。
1.创建一个StubActivity 且注册
2.封装数据到intent里,由stubActivity携带,且可以通过验证。等页面要启动的时候,把携带的数据提取出来,把页面替换。
/**
* 把Assets里面得文件复制到 /data/data/files 目录下
*
* @param context
* @param sourceName
*/
public static void extractAssets(Context context, String sourceName) {
AssetManager am = context.getAssets();
InputStream is = null;
FileOutputStream fos = null;
try {
is = am.open(sourceName);
File extractFile = context.getFileStreamPath(sourceName);
fos = new FileOutputStream(extractFile);
byte[] buffer = new byte[1024];
int count = 0;
while ((count = is.read(buffer)) > 0) {
fos.write(buffer, 0, count);
}
fos.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
closeSilently(is);
closeSilently(fos);
}
}
2.读取dex 生成对应的classloader
File extractFile = this.getFileStreamPath(apkName);
dexpath = extractFile.getPath();
fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE
classLoader = new DexClassLoader(dexpath,
fileRelease.getAbsolutePath(), null, getClassLoader());
3.通过classloader的loadclass方法去加载dex中任何一个类。
try {
mLoadClassDynamic = classLoader.loadClass("com.example.plugin1.Dynamic");
Object dynamicObject = mLoadClassDynamic.newInstance();
IDynamic dynamic = (IDynamic) dynamicObject;
String content = dynamic.getStringForResId(MainActivity.this);
tv.setText(content);
Toast.makeText(getApplicationContext(), content + "", Toast.LENGTH_LONG).show();
} catch (Exception e) {
Log.e("DEMO", "msg:" + e.getMessage());
}
provided 代替complie,好处是编译的时候用到对应的jar包,打包成apk并不会在apk中存在。provided 只支持jar包。
res下可编译的资源文件
assets目录下存放的原始资源文件
获取assets目录下所有文件
AssetManager assets = getResources().getAssets();
final String[] list = assets.list("");
1.通过反射创建AssetManager对象,调用addAssetPath方法,把插件的路径添加到AssetManager对象中,这个AssetManager只为Plugin服务
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexpath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
2.重写Activity的getAsset,getResource getTheme方法
3.加载外部插件,生成该插件的classLoader对象。
File extractFile = this.getFileStreamPath(apkName);
dexpath = extractFile.getPath();
fileRelease = getDir("dex", 0); //0 表示Context.MODE_PRIVATE
classLoader = new DexClassLoader(dexpath,fileRelease.getAbsolutePath(), null, getClassLoader());
4.通过反射,拿到插件中的类,构造处插件类的对象Dynamic接口对象,然后调用方法。