你們好,我叫小鑫,也能夠叫我蠟筆小鑫😊;html
本人17年畢業於中山大學,於2018年7月加入37手遊安卓團隊,曾經就任於久邦數碼擔任安卓開發工程師;java
目前是37手遊安卓團隊的海外負責人,負責相關業務開發;同時兼顧一些基礎建設相關工做。android
一個運行的App做爲宿主,去加載一個未安裝的apk文件,而且運行起來,這就叫作插件化api
插件化的使用場景:數組
一、線上新增功能(如淘寶、支付寶等)markdown
二、熱修復(經過下發補丁插件,完成對功能的修復)app
三、當編譯太慢時,可使用插件化,對某些不改動的代碼作成插件,加快編譯速度框架
一、插件聽從宿主的定義的標準,使用宿主的上下文環境。ide
二、優勢:只使用了少許反射,無hook,實現簡單函數
三、缺點:在插件中只能使用宿主提供的上下文環境,如插件Activity中,不能使用this當上下文,也就是說有必定侵入性,須要修改插件Activity的實現。
一、定義宿主的標準,下面以Activity爲例
public interface ActivityInterface {
/** * 把宿主(app)的環境 給 插件 * @param appActivity */
void insertAppContext(Activity appActivity);
// 生命週期方法
void onCreate(Bundle savedInstanceState);
void onStart();
void onResume();
void onDestroy();
//此處省略了其餘聲明週期,只作演示使用
}
複製代碼
二、在插件模塊中,根據標準實現插件Activity
//根據標準實現的插件模塊中的BaseActivity
public class BaseActivity implements ActivityInterface {
//宿主傳遞過來的上下文
public Activity appActivity; // 宿主的環境
@Override
public void insertAppContext(Activity appActivity) {
this.appActivity = appActivity;
}
@SuppressLint("MissingSuperCall")
@Override
public void onCreate(Bundle savedInstanceState) {
}
@SuppressLint("MissingSuperCall")
@Override
public void onStart() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onResume() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onDestroy() {
}
//實際上走的是宿主的setContentView方法
public void setContentView(int resId) {
appActivity.setContentView(resId);
}
public View findViewById(int layoutId) {
return appActivity.findViewById(layoutId);
}
@Override
public void startActivity(Intent intent) {
Intent intentNew = new Intent();
intentNew.putExtra("className", intent.getComponent().getClassName()); // TestActivity 全類名
appActivity.startActivity(intentNew);
}
}
複製代碼
//BaseActivity是關鍵
public class PluginActivity extends BaseActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.plugin_main);
// this 會報錯,由於插件沒有安裝,也沒有組件的環境,因此必須使用宿主環境
Toast.makeText(appActivity, "我是插件", Toast.LENGTH_SHORT).show();
findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//這個startActivity走的是BaseActivity的startActivity,也是被代理過的
startActivity(new Intent(appActivity, TestActivity.class));
}
});
}
}
複製代碼
三、將插件模塊打包apk,在宿主中加載該apk
對插件apk的加載主要分爲兩個步驟,即加載類和加載資源。加載類使用的是自定義的DexClassLoader,加載資源使用的是反射調用AssetManager的addAssetPath方法。
具體的代碼以下:
public class PluginManager {
private static final String TAG = PluginManager.class.getSimpleName();
private static PluginManager pluginManager;
private Context context;
public static PluginManager getInstance(Context context) {
if (pluginManager == null) {
synchronized (PluginManager.class) {
if (pluginManager == null) {
pluginManager = new PluginManager(context);
}
}
}
return pluginManager;
}
public PluginManager(Context context) {
this.context = context;
}
private DexClassLoader dexClassLoader;
private Resources resources;
/** * 一、加載類 * 二、加載資源 */
public void loadPlugin() {
try {
File file = AssetUtils.copyAssetPlugin(context, "p.apk", "plugin");
if (!file.exists()) {
Log.d(TAG, "插件包 不存在...");
return;
}
String pluginPath = file.getAbsolutePath();
File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, context.getClassLoader());
// 加載資源
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPathMethod.invoke(assetManager, pluginPath); // 插件包的路徑 pluginPath
Resources r = context.getResources(); // 宿主的資源配置信息
// 特殊的 Resources,加載插件裏面的資源的 Resources
resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
}
public ClassLoader getClassLoader() {
return dexClassLoader;
}
public Resources getResources() {
return resources;
}
}
複製代碼
四、在宿主中定義佔位的Activity
這裏最重要的步驟爲:
一、重寫getResources和getClassLoader方法,使用插件的ClassLoader和插件的Resources
二、實例化出來插件Activity
三、給插件Activity注入上下文
四、調用插件Activity的onCreate方法
代碼以下:
public class ProxyActivity extends Activity {
//這裏使用的是插件中的資源
@Override
public Resources getResources() {
return PluginManager.getInstance(this).getResources();
}
//這裏使用的是插件中的類加載器
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance(this).getClassLoader();
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 真正的加載 插件裏面的 Activity
String className = getIntent().getStringExtra("className");
try {
Class mPluginActivityClass = getClassLoader().loadClass(className);
// 實例化 插件包裏面的 Activity
Constructor constructor = mPluginActivityClass.getConstructor(new Class[]{});
Object mPluginActivity = constructor.newInstance(new Object[]{});
ActivityInterface activityInterface = (ActivityInterface) mPluginActivity;
// 給插件注入上下文
activityInterface.insertAppContext(this);
Bundle bundle = new Bundle();
bundle.putString("appName", "我是宿主傳遞過來的信息");
// 執行插件裏面的onCreate方法
activityInterface.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className = intent.getStringExtra("className");
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra("className", className); // 包名+TestActivity
// 要給TestActivity 進棧
super.startActivity(proxyIntent);
}
}
複製代碼
至此,能夠實現簡單的佔位式插件化了,咱們來總結下步驟:
一、定義宿主和插件之間的標準,如Activity的標準爲IActivityInterface
二、根據標準實現插件模塊,打成apk文件(這裏最重要的是插件中使用的上下文是宿主中傳遞過來的)
三、宿主中加載插件模塊apk
四、定義佔位Activity,在OnCreate方法中,根據Intent攜帶的插件Activity信息,反射插件Activity實例,爲插件Activity注入佔位Activity的上下文,調用插件Activity實例的onCreate方法啓動
這種實現方式的優勢是:全程僅有少許的反射,並沒有hook系統操做,適配工做簡單。
缺點也很明顯,在插件Activity中,須要遵照宿主規則,若是要作成框架,侵入性這個問題難以解決
學完了佔位式插件化後,下面咱們來介紹一種在插件中可使用this的方式,採用hook系統api的方式實現插件化
一、插件中的Activity可使用this,與常規寫法無異,無需像佔位式那樣聽從標準
二、hook操做較多,主要有兩個環節須要hook,一個是hook欺騙AMS,啓動沒有再AndroidManifest中註冊的Activity。另外一個是hook實現將插件的dex和宿主的dex合併,替換掉原先的dexElements
一、startActivity的過程
從圖中看出,App告訴AMS啓動Activity時,是攜帶了Intent的,咱們平常看到的have you declared this activity in your AndroidManifest.xml
這個錯誤,就是在調用了startActivity後,AMS對要啓動的Activity進行檢查時觸發的
也就是說,若是咱們要欺騙AMS,在startActivity時,攜帶的Intent中的Activity就必須是一個在AndroidManifest中註冊的Activity,而不能是咱們插件中的Activity。
這裏怎麼辦?偷龍轉鳳,偷樑換柱,狸貓換太子~
解決方法是,將Intent中的Component暫時替換成一個佔位Activity(在AndroidManifest中聲明過的),而且將真正要啓動的插件Activity以參數的形式存放到Intent中。
在AMS向App發送LAUNCH_ACTIVITY事件時,把真正要啓動的Activity啓動
基本原理就是這樣了,那麼要解決的重點問題以下:
一、在調用Activity時,替換掉啓動插件Activity的Intent爲啓動佔位Activity的Intent,並將啓動插件Activity的Intent以參數存放到啓動佔位Activity的Intent中
二、在AMS發送LAUNCH_ACTIVITY事件時,攔截,將Intent換回啓動插件Activity的Intent
三、啓動Activity時,是用的默認的ClassLoader加載Activity類,反射實例化的,因此須要把插件Activity加入到默認的ClassLoader中
下面咱們挨個問題來解決
一、hook AMS,偷龍轉鳳
private void hookAmsAction() throws Exception {
Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");
// 咱們要拿到IActivityManager對象,才能讓動態代理裏面的 invoke 正常執行下
// 執行此方法 static public IActivityManager getDefault(),就能拿到 IActivityManager
Class mActivityManagerNativeClass2 = Class.forName("android.app.ActivityManagerNative");
final Object mIActivityManager = mActivityManagerNativeClass2.getMethod("getDefault").invoke(null);
// 動態代理IActivityManager
Object mIActivityManagerProxy = Proxy.newProxyInstance(
HookApplication.class.getClassLoader(),
new Class[]{mIActivityManagerClass}, // 要監聽的接口
new InvocationHandler() { // IActivityManager 接口的回調方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
// 用ProxyActivity 繞過了 AMS檢查
Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
// 把要啓動插件Activity的Intent當作參數存進去
intent.putExtra("actionIntent", ((Intent) args[2]));
args[2] = intent;
}
Log.d("hook", "攔截到了IActivityManager裏面的方法" + method.getName());
// 讓系統繼續正常往下執行
return method.invoke(mIActivityManager, args);
}
});
/** * 爲了拿到 gDefault * 經過 ActivityManagerNative 拿到 gDefault變量(對象) */
Class mActivityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = mActivityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true); // 受權
Object gDefault = gDefaultField.get(null);
// 替換點
Class mSingletonClass = Class.forName("android.util.Singleton");
// 獲取此字段 mInstance
Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
// 替換
mInstanceField.set(gDefault, mIActivityManagerProxy);
}
複製代碼
二、hook LAUNCH_ACTIVITY事件,將要啓動的Activity換回來
/** * Hook LuanchActivity,即將要實例化Activity,要把ProxyActivity 給 換回來 ---》 TestActivity */
private void hookLuanchActivity() throws Exception {
Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
mCallbackFiled.setAccessible(true); // 受權
/** * handler對象怎麼來 * 1.尋找H,先尋找ActivityThread * * 執行此方法 public static ActivityThread currentActivityThread() * * 經過ActivityThread 找到 H * */
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
// 得到ActivityThrea對象
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mHField = mActivityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
// 獲取真正對象
Handler mH = (Handler) mHField.get(mActivityThread);
mCallbackFiled.set(mH, new MyCallback(mH)); // 替換 增長咱們本身的實現代碼
}
public static final int LAUNCH_ACTIVITY = 100;
class MyCallback implements Handler.Callback {
private Handler mH;
public MyCallback(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
// 作咱們在本身的業務邏輯(把ProxyActivity 換成 TestActivity)
Object obj = msg.obj;
try {
// 咱們要獲取以前Hook攜帶過來的 TestActivity
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// 獲取 intent 對象,才能取出攜帶過來的 actionIntent
Intent intent = (Intent) intentField.get(obj);
// actionIntent == 插件Activity的Intent
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if (actionIntent != null) {
// 把ProxyActivity換成真正的插件Activity
intentField.set(obj, actionIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
//事件正常往下執行
mH.handleMessage(msg);
return true; // 系統不會往下執行
}
}
複製代碼
三、將插件dex和宿主dex合併
private void pluginToAppAction() throws Exception {
// 第一步:找到宿主 dexElements 獲得此對象 PathClassLoader表明是宿主
PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // 本質就是PathClassLoader
Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object mDexPathList = pathListField.get(pathClassLoader);
Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
// 本質就是 Element[] dexElements
Object dexElements = dexElementsField.get(mDexPathList);
/*** ---------------------- ***/
// 第二步:找到插件 dexElements 獲得此對象,表明插件 DexClassLoader--表明插件
File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File(pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
if (!file.exists()) {
throw new FileNotFoundException("沒有找到插件包!!: " + file.getAbsolutePath());
} else {
Log.i("ZXX", "找到插件: " + file.getAbsolutePath());
}
String pluginPath = file.getAbsolutePath();
File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); // data/data/包名/pluginDir/
DexClassLoader dexClassLoader = new
DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());
Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
// private final DexPathList pathList;
Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
pathListFieldPlugin.setAccessible(true);
Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);
Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
dexElementsFieldPlugin.setAccessible(true);
// 本質就是 Element[] dexElements
Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);
// 第三步:建立出 新的 dexElements []
int mainDexLeng = Array.getLength(dexElements);
int pluginDexLeng = Array.getLength(dexElementsPlugin);
int sumDexLeng = mainDexLeng + pluginDexLeng;
// 參數一:int[] String[] ... 咱們須要Element[]
// 參數二:數組對象的長度
// 本質就是 Element[] newDexElements
Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // 建立數組對象
// 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements
for (int i = 0; i < sumDexLeng; i++) {
// 先融合宿主
if (i < mainDexLeng) {
// 參數一:新要融合的容器 -- newDexElements
Array.set(newDexElements, i, Array.get(dexElements, i));
} else { // 再融合插件的
Array.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng));
}
}
// 第五步:把新的 newDexElements,設置到宿主中去
// 宿主
dexElementsField.set(mDexPathList, newDexElements);
// 處理加載插件中的佈局,這裏和佔位式一致
doPluginLayoutLoad();
}
複製代碼
hook插件化主要有三個重點步驟:
一、欺騙AMS,繞過AMS對插件Activity的檢測,主要是經過偷龍轉鳳的方式實現
二、hook AMS啓動Activity的LAUNCH_ACTIVITY事件,啓動插件Activity
三、將插件Dex和宿主Dex合併
這種方式實現的插件化,插件Activity中可使用this,侵入性低。但因爲用了許多hook操做,系統適配須要作的工做較多。重點的三個操做尤爲須要根據系統源碼作必定的適配
在hook實現插件化中,因爲是將全部的插件都加入到dexElements中,宿主和插件用的仍是同一個ClassLoader。下面咱們來介紹LoadedApk式實現插件化,這事一種使用多個ClassLoader實現的插件化
宿主和插件用的ClassLoader不是同一個
欺騙AMS和偷龍轉鳳的實現和hook式實現插件化是同樣的,不一樣的是hook式實現插件化是在BaseDexClassLoader中的dexElements中加入插件的dex,來達到成功加載插件類的目的。而LoadedApk式則不是。下面來分析LoadedApk式實現插件化的原理
查看ActivityThread中啓動Activity的代碼
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
//一、獲取LoadedApk
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
。。。省略
Activity activity = null;
try {
//二、從LoadedApk中獲取ClassLoader,用於加載Activity類
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//實例化Activity
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
。。。省略
複製代碼
獲取PackageInfo的代碼以下:
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) {
final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if (includeCode) {
//主要是這裏,根據包名從mPackages中獲取,那麼只要構造插件的LoadedApk對象,放入到mPackeges中就能實現
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
複製代碼
欺騙AMS的操做和偷龍轉風環節和hook一致,再也不贅述。
一、下面介紹如何構建LoadedApk對象,加入到ActivityThread中的mPackages中
/** * 本身創造一個LoadedApk.ClassLoader 添加到 mPackages,此LoadedApk 專門用來加載插件裏面的 class */
private void customLoadedApkAction() throws Exception {
File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File( pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
if (!file.exists()) {
throw new FileNotFoundException("插件包不存在..." + file.getAbsolutePath());
}
String pulginPath = file.getAbsolutePath();
// mPackages 添加 自定義的LoadedApk
// final ArrayMap<String, WeakReference<LoadedApk>> mPackages 添加自定義LoadedApk
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
// 執行此方法 public static ActivityThread currentActivityThread() 拿到 ActivityThread對象
Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);
Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
// 拿到mPackages對象
Object mPackagesObj = mPackagesField.get(mActivityThread);
Map mPackages = (Map) mPackagesObj;
// 如何自定義一個 LoadedApk,系統是如何創造LoadedApk的,咱們就怎麼去創造LoadedApk
// 執行此 public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo)
Class mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
Field defaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
defaultField.setAccessible(true);
Object defaultObj = defaultField.get(null);
/** * ApplicationInfo 如何獲取,咱們以前學習 APK解析源碼分析 */
ApplicationInfo applicationInfo = getApplicationInfoAction();
Method mLoadedApkMethod = mActivityThreadClass.getMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); // 類類型
// 執行 才能拿到 LoedApk 對象
Object mLoadedApk = mLoadedApkMethod.invoke(mActivityThread, applicationInfo, defaultObj);
// 自定義加載器 加載插件
// String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent
File fileDir = getDir("pulginPathDir", Context.MODE_PRIVATE);
// 自定義 加載插件的 ClassLoader
ClassLoader classLoader = new PluginClassLoader(pulginPath,fileDir.getAbsolutePath(), null, getClassLoader());
Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
mClassLoaderField.setAccessible(true);
mClassLoaderField.set(mLoadedApk, classLoader); // 替換 LoadedApk 裏面的 ClassLoader
// 添加自定義的 LoadedApk 專門加載 插件裏面的 class
// 最終的目標 mPackages.put(插件的包名,插件的LoadedApk);
WeakReference weakReference = new WeakReference(mLoadedApk); // 放入 自定義的LoadedApk --》 插件的
mPackages.put(applicationInfo.packageName, weakReference); // 增長了咱們本身的LoadedApk
}
/** * 獲取 ApplicationInfo 爲插件服務的 * @return * @throws */
private ApplicationInfo getApplicationInfoAction() throws Exception {
// 執行此public static ApplicationInfo generateApplicationInfo方法,拿到ApplicationInfo
Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");
Object mPackageParser = mPackageParserClass.newInstance();
// generateApplicationInfo方法的類類型
Class $PackageClass = Class.forName("android.content.pm.PackageParser$Package");
Class mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Method mApplicationInfoMethod = mPackageParserClass.getMethod("generateApplicationInfo",$PackageClass,
int.class, mPackageUserStateClass);
File dirFile = getDir("plugin", Context.MODE_PRIVATE);
File file = new File(dirFile.getAbsoluteFile() + File.separator + "p.apk");
String pulginPath = file.getAbsolutePath();
// 執行此public Package parsePackage(File packageFile, int flags)方法,拿到 Package
// 得到執行方法的對象
Method mPackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
Object mPackage = mPackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);
// 參數 Package p, int flags, PackageUserState state
ApplicationInfo applicationInfo = (ApplicationInfo)
mApplicationInfoMethod.invoke(mPackageParser, mPackage, 0, mPackageUserStateClass.newInstance());
// 得到的 ApplicationInfo 就是插件的 ApplicationInfo
// 咱們這裏獲取的 ApplicationInfo
// applicationInfo.publicSourceDir = 插件的路徑;
// applicationInfo.sourceDir = 插件的路徑;
applicationInfo.publicSourceDir = pulginPath;
applicationInfo.sourceDir = pulginPath;
return applicationInfo;
}
複製代碼
二、hook AMS啓動Activity的回調
class MyCallback implements Handler.Callback {
private Handler mH;
public MyCallback(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case LAUNCH_ACTIVITY:
// 作咱們在本身的業務邏輯(把ProxyActivity 換成 TestActivity)
Object obj = msg.obj; // 本質 ActivityClientRecord
try {
// 咱們要獲取以前Hook攜帶過來的 TestActivity
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// 獲取 intent 對象,才能取出攜帶過來的 actionIntent
Intent intent = (Intent) intentField.get(obj);
Intent actionIntent = intent.getParcelableExtra("actionIntent");
if (actionIntent != null) {
intentField.set(obj, actionIntent); // 把ProxyActivity 換成 插件Activity
/*** * 咱們在如下代碼中,對插件 和 宿主 進行區分 */
Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
activityInfoField.setAccessible(true); //受權
ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);
// 宿主的Intent的getPackage會拿到包名,插件的會是空,用來判斷是不是插件Intent
if (actionIntent.getPackage() == null) {
//將applicationInfo的包名改成插件的包名,這樣拿到的LoadedApk纔是咱們自定義的
activityInfo.applicationInfo.packageName = actionIntent.getComponent().getPackageName();
// 這個是下個步驟,hook PMS,繞過PMS的檢測
hookGetPackageInfo();
} else { // 宿主
activityInfo.applicationInfo.packageName = actionIntent.getPackage();
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
mH.handleMessage(msg);
// 讓系統繼續正常往下執行
// return false; // 系統就會往下執行
return true; // 系統不會往下執行
}
複製代碼
三、hook PMS,繞過檢測
光這樣操做還不行,在Activity啓動時,PMS會檢測包名對應的Apk是否有安裝(LoadedApk中的initializeJavaContextClassLoader方法),沒有安裝會報錯。
調用流程:performLaunchActivity->makeApplication->initializeJavaContextClassLoader。
initializeJavaContextClassLoader()方法代碼以下:
IPackageManager pm = ActivityThread.getPackageManager();
android.content.pm.PackageInfo pi;
try {
pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
UserHandle.myUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
if (pi == null) {
throw new IllegalStateException("Unable to get package info for "
+ mPackageName + "; is package not installed?");
}
複製代碼
所以還須要hook PMS,繞過檢測,實現代碼以下:
// Hook 攔截此 getPackageInfo 作本身的邏輯
private void hookGetPackageInfo() {
try {
// sPackageManager 替換 咱們本身的動態代理
Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = mActivityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Field sPackageManagerField = mActivityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
final Object packageManager = sPackageManagerField.get(null);
/** * 動態代理 */
Class mIPackageManagerClass = Class.forName("android.content.pm.IPackageManager");
Object mIPackageManagerProxy = Proxy.newProxyInstance(getClassLoader(),
new Class[]{mIPackageManagerClass}, // 要監聽的接口
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("getPackageInfo".equals(method.getName())) {
// 如何才能繞過 PMS, 欺騙系統
// pi != null
return new PackageInfo(); // 成功繞過 PMS檢測
}
// 讓系統正常繼續執行下去
return method.invoke(packageManager, args);
}
});
// 替換 狸貓換太子 換成咱們本身的 動態代理
sPackageManagerField.set(null, mIPackageManagerProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
複製代碼
LoadedApk方式實現插件化,主要在啓動Activity時,加載這個環節和hook方式不一樣。
主要步驟是:
模仿系統源碼,實現插件apk的LoadedApk實例。並放置到ActivityThread中的mPackages對象中
在ActivityThread的mH回調中,偷龍轉鳳插件Activity的同時,將activityInfo中的applicationInfo中的包名替換爲插件的包名,從而讓後續邏輯使用插件的LoadedApk
最後,hook PMS,繞過PMS對插件是否安裝的檢測
一、佔位式實現插件化是比較穩定的,兼容性較好,由於沒有hook系統的api。但因爲要時刻注意使用宿主的上下文,編寫插件是比較難受的
二、hook方式實現插件化,不用考慮宿主環境,可是對系統api進行了hook,兼容性較差。
三、LoadedApk方式實現插件化,和hook方式接近,不用考慮宿主環境,但對系統api進行了hook,兼容性較差
美團Robust插件化的實現方式不像上述講的三種方式,而是借鑑的instant run的實現,在在編譯打包階段對每一個函數都插入一段控制邏輯代碼
簡單看下修復流程:
下面的方法,在編譯期間會加入一段控制邏輯,判斷是走補丁操做,仍是原操做
public long getIndex() {
return 100;
}
複製代碼
編譯後的getIndex
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封裝了獲取當前className和methodName的邏輯,並在其內部最終調用了changeQuickRedirect的對應函數
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
複製代碼
加載補丁時,會反射給changeQuickRedirect設置實例,當該實例不爲空,則走插件補丁的邏輯
固然不是這麼簡單就能完美實現,具體可參考:tech.meituan.com/2016/09/14/…
騰訊的QZone和Tinker本質上使用的是hook方式實現的插件化,操做的是dexElements
這裏咱們介紹下類的verify:
在apk安裝時,虛擬機會將classes.dex優化成odex文件,而後纔會執行。在這個過程當中,會進行類的verify操做,若是調用關係的類都在同一個dex,就會被打上CLASS_ISPREVERIFIED
標誌,而後寫入odex文件。
而在運行時,若是是被打上了標誌的類引用了其餘dex的類,則會報錯。
所以要解決打上標誌這個問題。
QZone的作法是:在每一個類的構造方法中,去引用一個其餘Dex中的類,從而避免被打上標誌
而Tinker的作法是:將宿主dex和插件dex進行合併,而後將dexElements中的就dex刪除,將合併後的dex加入。全部的代碼都在同一個dex中,也就不會有CLASS_ISPREVERIFIED
的問題了
SDK插件化本質上和app的插件化區別不大,不過SDK若是四大組件很少,很是建議使用佔位式插件化,由於兼容性問題較少。
但因爲sdk一般依附於宿主Activity調用,但最好不要對宿主Activity的getClassLoader和getResources進行處理,這樣能夠避免影響宿主的邏輯。因而能夠實現一個具有插件ClassLoader和Resouces的Context給Sdk使用
public class SQwanCore implements ISQwanCore {
@Override
public void init(Context context) {
//構造一個帶有插件classLoader和Resources的Context
SdkContextProxy sdkContext = new SdkContextProxy(context);
try {
ISQwanCore sdkObj = (ISQwanCore) sdkContext.getClassLoader().loadClass("com.sq.plugin.PluginSQwanCore").newInstance();
sdkObj.init(sdkContext);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
SdkContextProxy代碼以下:
public class SdkContextProxy extends ContextWrapper {
private Context baseContext;
public SdkContextProxy(Context base) {
super(base);
baseContext = base;
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance(baseContext).getClassLoader();
}
@Override
public Resources getResources() {
return PluginManager.getInstance(baseContext).getResources();
}
//啓動Activity時,作特殊操做,引導到ProxyActivity,參照佔位式的ProxyActivity
@Override
public void startActivity(Intent intent) {
String className = intent.getComponent().getClassName();
Intent proxyIntent = new Intent(this, ProxyActivity.class);
proxyIntent.putExtra("className", className); // 包名+插件Activity
// 要給插件Activity進棧
super.startActivity(proxyIntent);
}
}
複製代碼
Sdk的插件實現:
public class PluginSQwanCore implements ISQwanCore {
@Override
public void init(Context context) {
//PluginActivity要根據標準IActivityInterface實現
context.startActivity(new Intent(context, PluginActivity.class));
}
}
複製代碼
其餘邏輯參照佔位式插件化實現便可
至於SDK插件化的CLASS_ISPREVERIFIED
最簡單的處理方式是:將插件和宿主中共有的類,在插件中刪除便可,如本案例中ISQwanCore類在插件中刪除。
常見問題:
AssetManager的適配(19以上和如下不一樣)
能代理的和不能代理的(xml文件中的資源引用)
隔離宿主和插件的Resources(利用ContextWrapper處理)
資源ID衝突怎麼處理(使用gradle修改資源ID)
getIdentifier衝突怎麼處理(插件內優先插件,ResourceWrapper使用)
將宿主和插件資源合併出一個大的Resources,爲何?
一、宿主的資源中是包含系統資源的,這塊須要用
二、sdk有些資源放在宿主中,方便切包(如閃屏圖)
public class SuperHostResources {
private Context mContext;
private Resources mResources;
public SuperHostResources(Context context, String pluginPath) {
mContext = context;
mResources = buildHostResources(pluginPath);
}
private Resources buildHostResources(String pluginPath) {
Resources hostResources = mContext.getResources();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
try {
AssetManager assetManager = mContext.getResources().getAssets();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pluginPath);
hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
hostResources = mContext.getResources();
}
} else {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, pluginPath);
//衝突了不行的,要改id後才能夠這麼作
String baseApkPath = mContext.getApplicationInfo().sourceDir;
addAssetPathMethod.invoke(assetManager, baseApkPath);
hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
hostResources = mContext.getResources();
}
}
return hostResources;
}
public Resources get() {
return mResources;
}
}
複製代碼
使用ResourceWrapper,爲何?
一、如getIdentifier,能夠在這裏操做出優先加載插件,仍是優先加載宿主
public class MixResources extends ResourcesWrapper {
private Resources mPluginResources;
private String mPluginPkgName;
public MixResources(Resources hostResources, Context context, String pluginPath) {
super(hostResources);
PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);
mPluginResources = pluginResourcesBuilder.get();
mPluginPkgName = pluginResourcesBuilder.getPkgName();
}
public MixResources(Resources hostResources, Resources pluginResources, String pluginPkgName) {
super(hostResources);
mPluginResources = pluginResources;
mPluginPkgName = pluginPkgName;
}
public String getPluginPkgName() {
return mPluginPkgName;
}
@Override
public CharSequence getText(int id) throws Resources.NotFoundException {
try {
return super.getText(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getText(id);
}
}
@Override
public String getString(int id) throws Resources.NotFoundException {
try {
return super.getString(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getString(id);
}
}
@Override
public String getString(int id, Object... formatArgs) throws Resources.NotFoundException {
try {
return super.getString(id,formatArgs);
} catch (Resources.NotFoundException e) {
return mPluginResources.getString(id,formatArgs);
}
}
@Override
public float getDimension(int id) throws Resources.NotFoundException {
try {
return super.getDimension(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDimension(id);
}
}
@Override
public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelOffset(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDimensionPixelOffset(id);
}
}
@Override
public int getDimensionPixelSize(int id) throws Resources.NotFoundException {
try {
return super.getDimensionPixelSize(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDimensionPixelSize(id);
}
}
@Override
public Drawable getDrawable(int id) throws Resources.NotFoundException {
try {
return super.getDrawable(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDrawable(id);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getDrawable(id, theme);
} catch (Resources.NotFoundException e) {
return mPluginResources.getDrawable(id,theme);
}
}
@Override
public Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {
try {
return super.getDrawableForDensity(id, density);
} catch (Resources.NotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return mPluginResources.getDrawableForDensity(id, density);
} else {
return null;
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {
try {
return super.getDrawableForDensity(id, density, theme);
} catch (Exception e) {
return mPluginResources.getDrawableForDensity(id,density,theme);
}
}
@Override
public int getColor(int id) throws Resources.NotFoundException {
try {
return super.getColor(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColor(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getColor(id,theme);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColor(id,theme);
}
}
@Override
public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {
try {
return super.getColorStateList(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColorStateList(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {
try {
return super.getColorStateList(id,theme);
} catch (Resources.NotFoundException e) {
return mPluginResources.getColorStateList(id,theme);
}
}
@Override
public boolean getBoolean(int id) throws Resources.NotFoundException {
try {
return super.getBoolean(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getBoolean(id);
}
}
@Override
public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {
try {
return super.getLayout(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getLayout(id);
}
}
@Override
public String getResourceName(int resid) throws Resources.NotFoundException {
try {
return super.getResourceName(resid);
} catch (Resources.NotFoundException e) {
return mPluginResources.getResourceName(resid);
}
}
@Override
public int getInteger(int id) throws Resources.NotFoundException {
try {
return super.getInteger(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getInteger(id);
}
}
@Override
public CharSequence getText(int id, CharSequence def) {
try {
return super.getText(id,def);
} catch (Resources.NotFoundException e) {
return mPluginResources.getText(id,def);
}
}
@Override
public InputStream openRawResource(int id) throws Resources.NotFoundException {
try {
return super.openRawResource(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.openRawResource(id);
}
}
@Override
public XmlResourceParser getXml(int id) throws Resources.NotFoundException {
try {
return super.getXml(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getXml(id);
}
}
@Override
public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {
try {
super.getValue(id, outValue, resolveRefs);
} catch (Resources.NotFoundException e) {
mPluginResources.getValue(id, outValue, resolveRefs);
}
}
@Override
public Movie getMovie(int id) throws Resources.NotFoundException {
try {
return super.getMovie(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getMovie(id);
}
}
@Override
public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {
try {
return super.getAnimation(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.getAnimation(id);
}
}
@Override
public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {
try {
return super.openRawResource(id,value);
} catch (Resources.NotFoundException e) {
return mPluginResources.openRawResource(id,value);
}
}
@Override
public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
try {
return super.openRawResourceFd(id);
} catch (Resources.NotFoundException e) {
return mPluginResources.openRawResourceFd(id);
}
}
@Override
public int getIdentifier(String name, String defType, String defPackage) {
int pluginId = super.getIdentifier(name, defType, defPackage);
if (pluginId <= 0) {
return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
}
return pluginId;
}
public int getIdentifierFromPlugin(String name, String defType) {
return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
}
}
複製代碼
一、本文介紹了三種常見的插件化實現方案,包括佔位式、hook式、LoadedApk式以及他們各自的特色
二、介紹了市面上常見的插件化框架的方案,其中簡要介紹瞭如何避免CLASS_ISPREVERIFIED
問題
三、介紹SDK的插件化,SDK的Context的實現方案