自己封裝一個外掛化框架
一 概述
研究了一下滴滴開源的外掛化框架,感覺功能挺強大的,於是就想自己動手也封裝一個,不過相對於滴滴是支援四大元件的,我這裡就只對activity做了支援.要想知道怎麼載入一個外掛的activity,就得對activity的啟動過程有所瞭解,如果不懂的可以看一下Activity的啟動過程這篇文章.從這篇文章的分析得知,Activity的檢測工作是在WMS中進行的,所以我們只要使用佔坑的方法,先在清單檔案中註冊一個沒用的activity取名為A,然後在進入WMS之前將要啟動的activity的包名和類名替換成A的資訊,這樣在WMS中就可以逃避檢查了.當在WMS檢測完acitivty的資訊要建立acitivty時在將資訊替換成我們真正要建立的包名和類名.從之前的文章中分析得知,只要通過反射替換Instrumentation的execStartActivity和newActivity方法即可.
二 載入外掛
想要啟動外掛中的activity,首先就要將apk的資訊載入進來,通過PackageManagerService服務得知載入一個apk檔案是通過PackageParser來完成的,但是PackageParser是一個隱藏的類,在我們的引用程式中是呼叫不到的,所以只能從原始碼中拷貝到自己的專案中來.然後呼叫PackageParser的parsePackage(apk, flags)方法就可以得到這個apk的Package資訊了.
PackageParser.Package mPackage = PackageParserManager.parsePackage(mHostContext, apk, PackageParser.PARSE _MUST_BE_APK);
public class PackageParserManager {
public static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags) throws PackageParser.PackageParserException {
if (Build.VERSION.SDK_INT >= 24) {
return PackageParserV24.parsePackage(context, apk, flags);
} else if (Build.VERSION.SDK_INT >= 21) {
return PackageParserLollipop.parsePackage(context, apk, flags);
} else {
return PackageParserLegacy.parsePackage(context, apk, flags);
}
}
private static final class PackageParserV24 {
static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws PackageParser.PackageParserException {
PackageParser parser = new PackageParser();
PackageParser.Package pkg = parser.parsePackage(apk, flags);
ReflectUtil.invokeNoException(PackageParser.class, null, "collectCertificates",
new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
return pkg;
}
}
private static final class PackageParserLollipop {
static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags) throws PackageParser.PackageParserException {
PackageParser parser = new PackageParser();
PackageParser.Package pkg = parser.parsePackage(apk, flags);
try {
parser.collectCertificates(pkg, flags);
} catch (Throwable e) {
// ignored
}
return pkg;
}
}
private static final class PackageParserLegacy {
static final PackageParser.Package parsePackage(Context context, File apk, int flags) {
PackageParser parser = new PackageParser(apk.getAbsolutePath());
PackageParser.Package pkg = parser.parsePackage(apk, apk.getAbsolutePath(), context.getResources().getDisplayMetrics(), flags);
ReflectUtil.invokeNoException(PackageParser.class, parser, "collectCertificates",
new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
return pkg;
}
}
}
通過上面PackageParserManager 的parsePackage方法就可以將外掛apk載入進來封裝成Package,PackageParserManager 只是對PackageParser 在不同版本的封裝,PackageParser 的原始碼太長就不貼了,後面我會將專案的原始碼提供.
三.封裝外掛Apk
public class LoadPlugin {
private final File mNativeLibDir;
private Context mHostContext;
private final PackageParser.Package mPackage;
private final PackageInfo mPackageInfo;
private AssetManager mAssets;
private Resources mResources;
private ClassLoader mClassLoader;
private Context mPluginContext;
private Map<ComponentName, ActivityInfo> mActivityInfos;
private Application mApplication;
public LoadPlugin(Context context, File apk) throws PackageParser.PackageParserException {
this.mHostContext = context;
//載入外掛apk
mPackage = PackageParserManager.parsePackage(mHostContext, apk, PackageParser.PARSE_MUST_BE_APK);
mPackageInfo = new PackageInfo();
this.mPackageInfo.applicationInfo = this.mPackage.applicationInfo;
this.mPackageInfo.applicationInfo.sourceDir = apk.getAbsolutePath();
this.mPackageInfo.signatures = this.mPackage.mSignatures;
this.mPackageInfo.packageName = this.mPackage.packageName;
this.mPackageInfo.versionCode = this.mPackage.mVersionCode;
this.mPackageInfo.versionName = this.mPackage.mVersionName;
this.mPackageInfo.permissions = new PermissionInfo[0];
//封裝外掛的context
this.mPluginContext = new PluginContext(this,mHostContext);
//建立外掛的resource
this.mResources = createResources(context, apk);
this.mNativeLibDir = context.getDir(Constants.NATIVE_DIR, Context.MODE_PRIVATE);
//設定外掛的assets
this.mAssets = this.mResources.getAssets();
//建立外掛的類載入器
this.mClassLoader = createClassLoader(context, apk, this.mNativeLibDir, context.getClassLoader());
Map<ComponentName, ActivityInfo> activityInfos = new HashMap<ComponentName, ActivityInfo>();
for (PackageParser.Activity activity : this.mPackage.activities) {
activityInfos.put(activity.getComponentName(), activity.info);
}
this.mActivityInfos = Collections.unmodifiableMap(activityInfos);
this.mPackageInfo.activities = activityInfos.values().toArray(new ActivityInfo[activityInfos.size()]);
}
@WorkerThread
private static Resources createResources(Context context, File apk) {
if (Constants.COMBINE_RESOURCES) {
Resources resources = new ResourcesManager().createResources(context, apk.getAbsolutePath());
ResourcesManager.hookResources(context, resources);
return resources;
} else {
Resources hostResources = context.getResources();
AssetManager assetManager = createAssetManager(context, apk);
return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
}
private static AssetManager createAssetManager(Context context, File apk) {
try {
AssetManager am = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, am, "addAssetPath", apk.getAbsolutePath());
return am;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
private ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) {
File dexOutputDir = context.getDir(Constants.OPTIMIZE_DIR, Context.MODE_PRIVATE);
String dexOutputPath = dexOutputDir.getAbsolutePath();
DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);
if (Constants.COMBINE_CLASSLOADER) {
try {
DexUtil.insertDex(loader,mHostContext);
} catch (Exception e) {
e.printStackTrace();
}
}
return loader;
}
public String getPackageName() {
return this.mPackage.packageName;
}
public AssetManager getAssets() {
return this.mAssets;
}
public Resources getResources() {
return this.mResources;
}
public ClassLoader getClassLoader() {
return this.mClassLoader;
}
public Context getHostContext() {
return this.mHostContext;
}
public Context getPluginContext() {
return this.mPluginContext;
}
public Resources.Theme getTheme() {
Resources.Theme theme = this.mResources.newTheme();
theme.applyStyle(PluginUtil.selectDefaultTheme(this.mPackage.applicationInfo.theme, Build.VERSION.SDK_INT), false);
return theme;
}
public void setTheme(int resid) {
try {
ReflectUtil.setField(Resources.class, this.mResources, "mThemeResId", resid);
} catch (Exception e) {
e.printStackTrace();
}
}
public String getPackageResourcePath() {
int myUid = Process.myUid();
ApplicationInfo appInfo = this.mPackage.applicationInfo;
return appInfo.uid == myUid ? appInfo.sourceDir : appInfo.publicSourceDir;
}
public String getCodePath() {
return this.mPackage.applicationInfo.sourceDir;
}
public ActivityInfo getActivityInfo(ComponentName componentName) {
return this.mActivityInfos.get(componentName);
}
public Application getApplication() {
return mApplication;
}
public void invokeApplication(Instrumentation instrumentation) {
if (mApplication != null) {
return;
}
mApplication = makeApplication(false, instrumentation);
}
public Intent getLaunchIntent() {
ContentResolver resolver = this.mPluginContext.getContentResolver();
Intent launcher = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
for (PackageParser.Activity activity : this.mPackage.activities) {
for (PackageParser.ActivityIntentInfo intentInfo : activity.intents) {
if (intentInfo.match(resolver, launcher, false, "") > 0) {
return Intent.makeMainActivity(activity.getComponentName());
}
}
}
return null;
}
public Intent getLeanbackLaunchIntent() {
ContentResolver resolver = this.mPluginContext.getContentResolver();
Intent launcher = new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
for (PackageParser.Activity activity : this.mPackage.activities) {
for (PackageParser.ActivityIntentInfo intentInfo : activity.intents) {
if (intentInfo.match(resolver, launcher, false, "") > 0) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(activity.getComponentName());
intent.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
return intent;
}
}
}
return null;
}
public ApplicationInfo getApplicationInfo() {
return this.mPackage.applicationInfo;
}
public PackageInfo getPackageInfo() {
return this.mPackageInfo;
}
private Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
if (null != this.mApplication) {
return this.mApplication;
}
String appClass = this.mPackage.applicationInfo.className;
if (forceDefaultAppClass || null == appClass) {
appClass = "android.app.Application";
}
try {
this.mApplication = instrumentation.newApplication(this.mClassLoader, appClass, this.getPluginContext());
instrumentation.callApplicationOnCreate(this.mApplication);
return this.mApplication;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
class ResourcesManager {
public static synchronized Resources createResources(Context hostContext, String apk) {
Resources hostResources = hostContext.getResources();
Resources newResources = null;
AssetManager assetManager;
try {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
assetManager = AssetManager.class.newInstance();
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
} else {
assetManager = hostResources.getAssets();
}
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);
/* List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
for (LoadedPlugin plugin : pluginList) {
ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", plugin.getLocation());
}*/
if (isMiUi(hostResources)) {
newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
} else if (isVivo(hostResources)) {
newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
} else if (isNubia(hostResources)) {
newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
} else if (isNotRawResources(hostResources)) {
newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
} else {
// is raw android resources
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
} catch (Exception e) {
e.printStackTrace();
}
return newResources;
}
public static void hookResources(Context base, Resources resources) {
if (Build.VERSION.SDK_INT >= 24) {
return;
}
try {
ReflectUtil.setField(base.getClass(), base, "mResources", resources);
Object loadedApk = ReflectUtil.getPackageInfo(base);
ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);
Object activityThread = ReflectUtil.getActivityThread(base);
Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");
Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");
Object key = map.keySet().iterator().next();
map.put(key, new WeakReference<>(resources));
} catch (Exception e) {
e.printStackTrace();
}
}
private static boolean isMiUi(Resources resources) {
return resources.getClass().getName().equals("android.content.res.MiuiResources");
}
private static boolean isVivo(Resources resources) {
return resources.getClass().getName().equals("android.content.res.VivoResources");
}
private static boolean isNubia(Resources resources) {
return resources.getClass().getName().equals("android.content.res.NubiaResources");
}
private static boolean isNotRawResources(Resources resources) {
return !resources.getClass().getName().equals("android.content.res.Resources");
}
private static final class MiUiResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Class resourcesClazz = Class.forName("android.content.res.MiuiResources");
Resources newResources = (Resources)ReflectUtil.invokeConstructor(resourcesClazz,
new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},
new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});
return newResources;
}
}
private static final class VivoResourcesCompat {
private static Resources createResources(Context hostContext, Resources hostResources, AssetManager assetManager) throws Exception {
Class resourcesClazz = Class.forName("android.content.res.VivoResources");
Resources newResources = (Resources)ReflectUtil.invokeConstructor(resourcesClazz,
new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},
new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});
ReflectUtil.invokeNoException(resourcesClazz, newResources, "init",
new Class[]{String.class}, hostContext.getPackageName());
Object themeValues = ReflectUtil.getFieldNoException(resourcesClazz, hostResources, "mThemeValues");
ReflectUtil.setFieldNoException(resourcesClazz, newResources, "mThemeValues", themeValues);
return newResources;
}
}
private static final class NubiaResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Class resourcesClazz = Class.forName("android.content.res.NubiaResources");
Resources newResources = (Resources)ReflectUtil.invokeConstructor(resourcesClazz,
new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},
new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});
return newResources;
}
}
private static final class AdaptationResourcesCompat {
private static Resources createResources(Resources hostResources, AssetManager assetManager) throws Exception {
Resources newResources;
try {
Class resourcesClazz = hostResources.getClass();
newResources = (Resources) ReflectUtil.invokeConstructor(resourcesClazz,
new Class[]{AssetManager.class, DisplayMetrics.class, Configuration.class},
new Object[]{assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()});
} catch (Exception e) {
newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}
return newResources;
}
}
}
因為我只是要替換一下acitivty,所以只封裝一下替換activity所需要的resource,assets,context,mClassLoader 等資訊,resource和assest分別表示了外掛apk的資源資訊,context是activity的上下文管理者,用來獲取資源,mClassLoader 是用來將載入相應的activity類檔案.
四.封裝外掛activity的Context
class PluginContext extends ContextWrapper {
private final LoadPlugin mPlugin;
public PluginContext(LoadPlugin plugin,Context context) {
super(context);
this.mPlugin = plugin;
}
@Override
public Context getApplicationContext() {
return this.mPlugin.getApplication();
}
@Override
public ApplicationInfo getApplicationInfo() {
return this.mPlugin.getApplicationInfo();
}
private Context getHostContext() {
return getBaseContext();
}
@Override
public ClassLoader getClassLoader() {
return this.mPlugin.getClassLoader();
}
@Override
public String getPackageName() {
return this.mPlugin.getPackageName();
}
@Override
public String getPackageResourcePath() {
return this.mPlugin.getPackageResourcePath();
}
@Override
public String getPackageCodePath() {
return this.mPlugin.getCodePath();
}
@Override
public Object getSystemService(String name) {
// intercept CLIPBOARD_SERVICE,NOTIFICATION_SERVICE
if (name.equals(Context.CLIPBOARD_SERVICE)) {
return getHostContext().getSystemService(name);
} else if (name.equals(Context.NOTIFICATION_SERVICE)) {
return getHostContext().getSystemService(name);
}
return super.getSystemService(name);
}
@Override
public Resources getResources() {
return this.mPlugin.getResources();
}
@Override
public AssetManager getAssets() {
return this.mPlugin.getAssets();
}
到時將activity的context替換成我們封裝的PluginContext 物件,這樣在activity啟動的時候獲取對應的資源資訊都是從我們LoadPlugin 中獲取到的資源來獲取.
五.定義Instrumentation子類VAInstrumentation
public class VAInstrumentation extends Instrumentation {
public static final String TAG = "VAInstrumentation";
public static final int LAUNCH_ACTIVITY = 100;
private Instrumentation mBase;
private Context mContext;
private LoadPlugin mLoadPlugin;
public VAInstrumentation(Instrumentation base,Context mContext,LoadPlugin loadPlugin) {
this.mBase = base;
this.mContext = mContext;
this.mLoadPlugin = loadPlugin;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
拿到要啟動的acitivty的包名和類名
String targetPackageName = intent.getComponent().getPackageName();
String targetClassName = intent.getComponent().getClassName();
如果要啟動的包名和當前應用的包名不同,表示是要啟動外掛的activity
if (!targetPackageName.equals(mContext.getPackageName()) ) {
將外掛的包名和類名,先用其他欄位存起來
intent.putExtra(Constants.KEY_IS_PLUGIN, true);
intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName);
intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
將intent的包名和類名替換成我們佔坑的acitivty資訊
dispatchStubActivity(intent);
}
ActivityResult result = null;
try {
Class[] parameterTypes = {Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class,
int.class, Bundle.class};
通過反射呼叫Instrumentation的execStartActivity方法.
result = (ActivityResult) ReflectUtil.invoke(Instrumentation.class, mBase,
"execStartActivity", parameterTypes,
who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
private void dispatchStubActivity(Intent intent) {
ComponentName component = intent.getComponent();
String targetClassName = intent.getComponent().getClassName();
ActivityInfo info = mLoadPlugin.getActivityInfo(component);
if (info == null) {
throw new RuntimeException("can not find " + component);
}
Resources.Theme themeObj = mLoadPlugin.getResources().newTheme();
themeObj.applyStyle(info.theme, true);
//這個就是佔坑的類名
String stubActivity = "com.lyf.pluginapk.SecondeActivity";
Log.i(TAG, String.format("dispatchStubActivity,[%s -> %s]", targetClassName, stubActivity));
替換intent中的包名和類名
intent.setClassName(mContext, stubActivity);
}
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (intent.getBooleanExtra(Constants.KEY_IS_PLUGIN, false)) {
//如果是啟動外掛的acitivty,就拿到真正要啟動的包名和類名
String targetClassName = PluginUtil.getTargetActivity(intent);
if (targetClassName != null) {
呼叫newActivity方法建立acitivty,不過這裡傳入的是外掛acitivty的類載入器,和外掛的包名,類名.所以建立的也就是外掛的acitivty
Activity activity = mBase.newActivity(mLoadPlugin.getClassLoader(), targetClassName, intent);
activity.setIntent(intent);
try {
// for 4.1+
ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", mLoadPlugin.getResources());
} catch (Exception ignored) {
// ignored.
}
return activity;
}
}
//如果不是啟動外掛,就直接啟動對應的acitivty
return mBase.newActivity(cl, className, intent);
}
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
final Intent intent = activity.getIntent();
這個方法,主要是在呼叫acitivty的onCreate之前,將外掛的resrouse,context等資訊替換成我們之前載入外掛獲取的相應資訊,避免找不到資源.
if (PluginUtil.isIntentFromPlugin(intent)) {
Context base = activity.getBaseContext();
try {
ReflectUtil.setField(base.getClass(), base, "mResources", mLoadPlugin.getResources());
ReflectUtil.setField(ContextWrapper.class, activity, "mBase", mLoadPlugin.getPluginContext());
ReflectUtil.setField(Activity.class, activity, "mApplication", mLoadPlugin.getApplication());
ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", mLoadPlugin.getPluginContext());
// set screenOrientation
ActivityInfo activityInfo = mLoadPlugin.getActivityInfo(PluginUtil.getComponent(intent));
if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
activity.setRequestedOrientation(activityInfo.screenOrientation);
}
} catch (Exception e) {
e.printStackTrace();
}
}
mBase.callActivityOnCreate(activity, icicle);
}
@Override
public Context getContext() {
return mBase.getContext();
}
@Override
public Context getTargetContext() {
return mBase.getTargetContext();
}
@Override
public ComponentName getComponentName() {
return mBase.getComponentName();
}
}
六.載入外掛,替換Instrumentation
獲取外掛的路徑
File apk = new File(Environment.getExternalStorageDirectory(), "Test3.apk");
if (apk.exists()) {
try {
載入外掛
loadPlugin = new LoadPlugin(this, apk);
//hook Instrumentation類 Instrumentation在Activitythread中
Instrumentation instrumentation = ReflectUtil.getInstrumentation(this);
VAInstrumentation vaInstrumentation = new VAInstrumentation(instrumentation,this,loadPlugin);
Object activityThread = ReflectUtil.getActivityThread(this);
替換Instrumentation為我們定義的vaInstrumentation
ReflectUtil.setInstrumentation(activityThread, vaInstrumentation);
//為了設定application,否則可以不呼叫
loadPlugin.invokeApplication(vaInstrumentation);
} catch (Exception e) {
System.out.println("載入失敗");
e.printStackTrace();
}
}
到這一步替換完instrumentation類,在啟動外掛中的acitivty,就可以啟動起來了.
七 總結
這只是我自己為了驗證滴滴的外掛原理而自己實現的一遍過程,想要研究原始碼的可以去看看滴滴的原始碼,同時我也將我實現的原始碼上傳到了github,點選下載原始碼