1. 程式人生 > >自己封裝一個外掛化框架

自己封裝一個外掛化框架

一 概述

研究了一下滴滴開源的外掛化框架,感覺功能挺強大的,於是就想自己動手也封裝一個,不過相對於滴滴是支援四大元件的,我這裡就只對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,點選下載原始碼