读后感:
以前公司也做过插件化的开发,偶然的一天网上逛书店,看到这本书,买来看看,到现在大概看了几章,感觉这本书差点意思。包含的东西很多,但是感觉里面的东西都不是太深,甚至有些地方个人感觉都是错误的。比如里面contentprovider的本质是把数据存储到数据库里。当然也有很多以前没有接触过的,也是有所收获的,同时也感谢作者的分享。主观感觉,不喜勿喷。欢迎指正。
笔记
1.插件化的昨天
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]。
2.android底层知识
- binder原理,binder目的是解决跨进程通信。分为client和server两个进程。
- AIDL 原理
- AMS activityManagerService管理四大组件。
- ActivityThread
- Context
- service工作原理
- broadcastReceiver工作原理 按照发送方式分三类:无序广播、有序广播、粘性广播
- contentProvider工作原理
- PMS PackageManagerService 获取apk包的信息。apk是一个zip压缩包,在文件头会记录压缩包的大小。apk在安装的时候都是解析apk中resource.arsc文件,这个文件存储资源的所有信息,包括在apk中的地址、大小。
- PackParser 系统重启,会重新安装所有得app,这个由PMS完成。PackParser主要用来解析清单文件来获取四大组件信息。PackageParser中有一个方法,接受一个apkFile的参数,可以是当前apk,也可以是外部apk。所有通过这个类,来读取外部apk的信息。
- ClassLoader 类加载器 有几个重要的类,子类BaseDexClassLoader,还有两个类似于孙子类,PathClassLoader DexClassLoader 构造器里有个optimizedDirectory参数用来加载dex文件的,且创建一个DexFile对象。
- MultiDex 主要是android5.0之前的版本开始出现 65535问题,整个程序的方法栈只能最多为65535个。后来谷歌退出MultiDex工具来解决该问题。把一个dex拆分成多个dex。
3.反射
- getClass() 得到是一个class对象。eg:User.class
- Class.forName(); 通过包名获取class对象。
4.代理模式
- 动态代理
静态代理太不灵活,一个对象对应一个代理,代理类就会很多。这时候就产生了动态代理Proxy.newproxyInstance(ClassLoader classLoader,Class<?>[] interfaces,InvocationHandler h ) - ActivityManagerNative的Hook
- PMS Hook
5.对startActivity方法进行Hook
- 创建一个baseActivity 重写startActivityForResult,进行拦截
- 对Activity的Instrumentation 的方法 execStartActivity关键点进行Hook
Activity.class 针对 mInstrumentation字段的execStartActivity进行hook
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()
- AMN的getDefault方法进行hook
在Instrumentation的execStartActivity方法进行hook,对ActivitymanagerNative.getDefault()方法,通过动态代理的形式获取getDefault()方法返回IActivitiyManger接口。
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<T>对象; 我们取出这个单例里面的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);
}
}
- 对H类的mCallback 字段进行hook
ActivityThread里H类可以进行通信,也就是启动页面。通过hook code=100 启动页面。依然使用动态代理去做。
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();
}
}
- 再次对 Instrumentation 的字段进行Hook
拦截ActivityThread类里Instrumentation字段,拦截它的newActivity方法和callActivityOnCreate方法
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);
}
}
- 对ActivityThread的Instrumentation字段进行hook
此处使用context.startActvity进行启动页面,进行hook,和上述方式类型。
启动没有声明的activity
AMS的逻辑涉及整个系统,所以无法hook,也不能hook。
所以hook就从AMS的入口和出口进行hook。
1.创建一个StubActivity 且注册
2.封装数据到intent里,由stubActivity携带,且可以通过验证。等页面要启动的时候,把携带的数据提取出来,把页面替换。
6.插件化技术基础知识
- 加载外部dex
1.服务器下载到本地,然后去加载apk里dex(也可以放到hostapp里的assets,程序运行,在复制到内部系统中,再读取dex)
/**
* 把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包。
- application 插件化解决方法 ,通过反射获取执行,但是缺点就是没有生命周期了。
7.资源初探
7.1资源分类
res下可编译的资源文件
assets目录下存放的原始资源文件
获取assets目录下所有文件
AssetManager assets = getResources().getAssets();
final String[] list = assets.list("");
7.2 Resources AssetManager
- AssetsManager 是获取assets文件夹的文件的管理器对象,addAssetsPath(path) 可以传入插件的apk路径。
- Resources 获取各种资源的核心对象。
- resources.arsc文件,apk打包产生的文件。其实是个hash表,存放每个十六进制值和资源的关系。
7.3 获取资源的方案
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接口对象,然后调用方法。