讀後感:
以前公司也做過插件化的開發,偶然的一天網上逛書店,看到這本書,買來看看,到現在大概看了幾章,感覺這本書差點意思。包含的東西很多,但是感覺裏面的東西都不是太深,甚至有些地方個人感覺都是錯誤的。比如裏面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]。
[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
2.android底層知識
- binder原理,binder目的是解決跨進程通信。分爲client和server兩個進程。
- AIDL 原理
- AMS activityManagerService管理四大組件。
- app startActivity啓動流程 https://blog.csdn.net/chentaishan/article/details/105180793
- 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接口對象,然後調用方法。