安卓移动架构08-手写DroidPlugin插件化框架

移动架构08-手写DroidPlugin插件化框架

一、插件化介绍

插件化就是一种让插件(第三方的APP)运行在宿主(自己的APP)中的技术。

插件本身是一个APP,安装以后就可以运行在Android系统中。使用插件化技术,不会把插件安装在Android系统中,而是安装在宿主中,由宿主管理所有的插件,相当于插件是宿主的一个模块。

插件化的好处:

  1. 集成海量插件。插件是第三方开发的,我们只需要遵循一定的规范,把插件集成到宿主中,就可以把插件当成自己的模块使用。
  2. 安全。插件是由第三方开发的,如果出现崩溃,不会导致宿主APP崩溃。因为每个插件都是运行在独立的进程中。
  3. 减小APK体积。在宿主APP中,我们只为插件提供了入口,只需要很少的代码就可以集成插件。并且插件APK文件并不包含在宿主APK中,只有当使用插件时,才会下载插件APK文件,安装到宿主中。所以插件基本不会增加宿主APK的体积。

插件化实现方案分为两种:插桩式和Hook式。

1.插桩式

典型代表:DynamicLoadApk

优点:稳定;

缺点:需要使用『that』而不是『this』,所有activity都需要继承自proxyAvtivity(proxyAvtivity负责管理所有activity的生命周期)。

2.Hook式

典型代表:DroidPlugin

特点:功能强大,使用简单。

Hook的思想:拦截系统消息,替换执行内容,也就是偷梁换柱。

Hook的难点:找到可以Hook的点。

下面,我就仿照DroidPlugin框架,来实现一个小型插件化框架。

二、手写插件化框架

DroidPlugin框架的核心思想可以分两点:1.添加Hook点;2.进程管理。

1.添加Hook点

添加Hook点是为了让插件组件运行在宿主中。

插件的组件是安装在宿主中,而启动插件组件还是系统来执行。系统会检查组件是否在清单文件注册,然后获取对应的calss文件创建组件。

所以添加Hook点,就是要跳过清单文件检查(插件的组件不可能注册在宿主的清单文化中),并加载插件的class文件。

下面,我们从Activity的启动流程来看,怎么添加Hook点。

1.1Activity的启动流程

我们先来看下系统是怎么启动一个Activity的?

第一步,我们调用Context.startActivity();

第二步,ActivityManager调用startActivity();

第三步,PMS验证Acitvity是否在清单文件中注册,然后发送消息给ActivityThread。

第四步,ActivityThread的mH处理PMS发送的消息,如果消息为100,就调用luanchActivity();

第五步,ActivityThread验证ApplicaitonInfo的包名Pname1,如果与上一次获取的包名Pname2不一致,就调用PackageManager.getPackageInfo()获取包名Pname3。如果Pname3不为null,就使用Pname3作为包名,如果Pname3为null,就使用Pname1作为包名。

第六步,根据第五步获取的包名,从ActivityThread.mPackages获取loadedApk对象,根据loadedAPK获取CalssLoader对象,使用CalssLoader对象创建Activity。

第七步,调用Activity的生命周期方法。

1.2找出Hook点

首先,我们要跳过清单文件检查。思路是,在PMS验证Acitvity时,使用一个代理Activity(已经在清单文件中注册的Activity)来代替,验证后,再换回原来的Activity。

步骤如下:

  1. Hook第二步,也就是Hook系统的ActivityManager。将Activity替换成代理Activity,并保存真实的Activity。
  2. Hook第四步,也就是Hook当前ActivityThread的mH。将代理Activity替换成真实的Activity。

然后,我们要加载插件的class文件。思路是,先将插件APK生成loadedApk对象,然后插入ActivityThread.mPackages中,然后将ApplicaitonInfo的包名修改为插件的包名,然后获取插件的loadedApk对象来创建Activity。

步骤如下:

  1. Hook第四步,也就是Hook当前ActivityThread的mH。将ApplicaitonInfo的包名修改为插件的包名
  2. Hook第五步,也就是Hook当前ActivityThread的sPackageManager。将PackageManager.getPackageInfo()的返回值改为null。
  3. Hook第六步,也就是Hook系统的ClassLoader对象。将插件APK的loadedApk对象插入ActivityThread.mPackages中。

2.插件管理

插件管理做两件事:1.进程管理;2.插件APK解析。

2.1进程管理

一个插件运行在宿主中,为了不想宿主和其它插件的运行,需要运行在单独的代理进程中。一个Activity有四种启动模式,如果是单例的启动模式,就需要多个相同启动模式的代理Activity,否则就不能同时打开多个单例的插件Activity。

所以,进程管理就是选择合适的代理进程和代理组件。实现分为两步:占坑和进程维护。

首先是占坑。占坑就是在清单文件中注册多个代理进程和代理Activity,并且区分进程。

步骤如下:

  1. 在清单文件中注册多个进程的Activity,Standard的代理Activity只要1个,其他模式的代理Activity需要多个。
  2. 为了识别代理进程,进程名统一以PluginP开头。为了识别代理组件,使用统一的action和category。
  3. 在清单文件中,预注册多个权限。避免插件APP得不到权限,而导致崩溃。

然后是进程维护。进程维护就是管理所有的代理进程和代理组件,为插件提供可用的代理组件。

步骤如下:

  1. 通过action和category查询所有的代理进程和组件,保存在StaticProcessList中;
  2. 维护正在运行的进程和组件。通过代理进程名标识一个正在运行的进程,通过包名表示代理进程运行的插件,将所有正在运行的代理进程和组件,保存在RunningProcessList中。
  3. 选择代理组件。打开插件组件时,需要选择代理组件。选择代理组件有三个原则:1.相同插件的组件使用相同的进程;2.插件组件的启动模式要和代理组件的一致;3.单例的代理组件只能代理运行一个插件组件。

2.2插件APK解析

插件APK解析分为两步:1.安装;2.解析。

系统使用PMS服务来安装、解析APK文件,我们模仿系统使用一个自定义的PMS来安装、解析APK文件。

首先是安装。安装就是插件APK文件放到指定目录下,我们统一放到/data/user/0/宿主包名/Plugin/插件包名/apk/下。

然后是解析。解析就是获取插件APK的包信息和注册的组件信息等。使用自定义的PackageParser来解析,需要做版本兼容。

下面,就说一下怎么具体实现。

三、添加Hook点

Hook点有4个:

  1. HookActivityManager:Hook系统的ActivityManager;
  2. HookMH:Hook当前ActivityThread的mH;
  3. HookPackageManager:Hook当前ActivityThread的sPackageManager;
  4. HookClassLoader:Hook系统的ClassLoader对象

Hook就是拦截系统消息,替换执行内容。所以我们把拦截和替换分开来说。

1.HookActivityManager

首先,Hook系统的ActivityManager,拦截它的startActivity(),将跳转目标设为代理组件,从而跳过PMS检查。
步骤如下:

  1. 通过反射获取系统的ActivityManagerNative的gDefault属性;
  2. 通过反射gDefault,获取IactivityManager属性
  3. 使用动态代理的IactivityManager对象,替换gDefault的IactivityManager属性
try {
    /**
     * 通过反射获取系统的ActivityManagerNative的gDefault属性
     */
    Class ActivityManagerNativecls = Class.forName("android.app.ActivityManagerNative");
    Field gDefault = ActivityManagerNativecls.getDeclaredField("gDefault");
    gDefault.setAccessible(true);
    //得到ActivityManagerNative的gDefault属性
    Object defaltValue = gDefault.get(null);

    //mInstance对象
    Class SingletonClass = Class.forName("android.util.Singleton");
    Field mInstance = SingletonClass.getDeclaredField("mInstance");
    //还原 IactivityManager对象  系统对象
    mInstance.setAccessible(true);
    Object iActivityManagerObject = mInstance.get(defaltValue);

    //保存系统对象
    setRealObj(iActivityManagerObject);

    Class IActivityManagerIntercept = Class.forName("android.app.IActivityManager");
    //动态代理iActivityManagerObject,对其进行扩展,增加意图替换的逻辑
    Object oldIactivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader()
            , new Class[]{IActivityManagerIntercept}
            , this);

    //使用动态代理的IactivityManager对象,替换gDefault的IactivityManager属性
    mInstance.set(defaltValue, oldIactivityManager);
} catch (Exception e) {
    e.printStackTrace();
}

然后,使用动态代理,来修改startActivity的参数,即修改Intent的ComponentName。

Intent intent = null;
//从startActivity方法的参数中查找Intent参数的索引
int index = findFirstIntentIndexInArgs(args);
if (args != null && args.length > 1 && index >= 0) {
    intent = (Intent) args[index];
}

Intent newIntent = new Intent();
//修改跳转目标为代理组件,用于跳过PMS检查
ComponentName componentName = selectProxyActivity(intent);
newIntent.setComponent(componentName);
//保存真实的意图
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
args[index] = newIntent;

2.HookMH

首先,Hook当前ActivityThread的mH,用来修改luanchActiity消息的回调,因为之前将跳转目标设为代理组件,所以现在需要将跳转目标还原。

注意:开启新进程打开插件时,需要对新进程的ActivityThread的mH进行Hook。

try {
    Class forName = Class.forName("android.app.ActivityThread");
    Field currentActivityThreadField = forName.getDeclaredField("sCurrentActivityThread");
    currentActivityThreadField.setAccessible(true);
    //获取系统的ActivityTread的sCurrentActivityThread属性
    Object activityThreadObj = currentActivityThreadField.get(null);

    Field handlerField = forName.getDeclaredField("mH");
    handlerField.setAccessible(true);
    //获取sCurrentActivityThread的mH对象
    Handler mH = (Handler) handlerField.get(activityThreadObj);

    Field callbackField = Handler.class.getDeclaredField("mCallback");
    callbackField.setAccessible(true);
    //使用自定义的mCallback对象替换mH的mCallback属性
    callbackField.set(mH, new HandlerMH(mH));
} catch (Exception e) {
    e.printStackTrace();
}

然后,使用自定义的Callback来替换ActivityThread的mH的Callback,用来修改luanchActiity消息的回调。开启新进程打开插件时,即时修改了applicationInfo.packageName,系统也会从宿主APK中查找组件,这样就会导致找不到插件的组件而崩溃。为了避免崩溃,设定第一次进行Hook时,即新建进程打开插件时,直接打开代理组件。

try {
    Field intentField = obj.getClass().getDeclaredField("intent");
    intentField.setAccessible(true);
    //代理意图
    Intent proxyIntent = (Intent) intentField.get(obj);
    //真实意图
    Intent oldIntent = proxyIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);

    //如果是第一次拦截,就退出
    if (times++ <= 1) {
        return;
    }

    //开启代理组件的进程时,初始自定义AMS中的代理进程的信息
    onOpenPluginProcess(proxyIntent, oldIntent);

    if (oldIntent != null) {
        //还原真实的意图
        proxyIntent.setComponent(oldIntent.getComponent());
        /**
         * 获取activityInfo对象
         */
        Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
        activityInfoField.setAccessible(true);
        ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);
        /**
         * 修改activityInfo的包名。如果是宿主的Activity,则不需要修改包名;如果是插件的Activity,就需要修改为插件的包名。
         * 因为使用loadedApk插入的方式加载插件的类时,会生成新的loadedApk对象,这个时候就需要根据插件的包名,从ActivityThrea中查找插件的loadedApk对象。
         */
        activityInfo.applicationInfo.packageName = proxyIntent.getComponent().getPackageName();

        //加载插件APK的loadedAPK。因为插件APK没有安装到系统中,是由自定义的PMS管理的,所以需要通把插件APK的loadedAPK对象插入到宿主中,即把插件的类插入到宿主中。
        PluginManager.preLoadApk(oldIntent.getComponent());
    }
} catch (Exception e) {
    e.printStackTrace();
}

3.HookPackageManager

首先,Hook当前ActivityThread的sPackageManager,修改它的getPackageInfo()。系统调用handleLuachActivity()时,会通过IPackageManage.getPackageInfo()检查Activity的包名。如果IPackageManage.getPackageInfo()返回的包名为null,则使用activityInfo.applicationInfo.packageName为Activitry的包名;如果IPackageManage.getPackageInfo()返回的包名不为null,则与activityInfo.applicationInfo.packageName比较,如果不同,就会报错。

现在我们需要设置Activity的包名为插件的包名,就需要拦截IPackageManage.getPackageInfo(),让IPackageManage.getPackageInfo()返回的包名为null。

注意:开启新进程打开插件时,需要对新进程的ActivityThread的mInstrumentation进行Hook。

try {
    /**
     * 获取系统的ActivityThread对象
     */
    Class activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

    /**
     * 获取ActivityThread的sPackageManager对象
     */
    Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
    sPackageManagerField.setAccessible(true);
    Object sPackageManager = sPackageManagerField.get(currentActivityThread);

    //保存系统对象
    setRealObj(sPackageManager);

    /**
     * 使用代理的IPackageManager对象,替换ActivityThread的sPackageManager对象
     */
    Class iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
    Object mPackageManager = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader()
            , new Class[]{iPackageManagerInterface}, this);
    sPackageManagerField.set(currentActivityThread, mPackageManager);
} catch (Exception e) {
    e.printStackTrace();
}

然后,使用动态代理,修改getPackageInfo().

PackageInfo packageInfo = new PackageInfo();
return packageInfo;

4.HookClassLoader

用来Hook系统的ClassLoader对象,由于不能直接修改ClassLoader对象,所以修改ClassLoader的上级对象loadedAPK,将插件APK的loadedPAK对象插入宿主的ActivityThread的mPackages中,然后从插件的loadedAPK中获取calssLoader,从而实现替换系统ClassLoader对象。因为插件APK没有安装到系统中,是由自定义的PMS管理的,所以需要通把插件APK的loadedAPK对象插入到宿主中,即把插件的类插入到宿主中。

try {
    /**
     * 获取系统的activityThread对象
     */
    Class activityThreadClass = Class.forName("android.app.ActivityThread");
    Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
    currentActivityThreadMethod.setAccessible(true);
    Object currentActivityThread = currentActivityThreadMethod.invoke(null);

    /**
     * 获取ActivityThread的mPackages对象
     * ActivityThread的mPackages对象是用来保存loadedApk对象,加载插件类就是把插件的loadedApk对象插到mPackages中
     */
    Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
    mPackagesField.setAccessible(true);
    Map mPackages = (Map) mPackagesField.get(currentActivityThread);

    /**
     * 生成插件的loadedApk对象
     * 使用ActivityThread的getPackageInfoNoCheck方法生成loadedApk对象,需要两个参数ApplicationInfo和CompatibilityInfo
     */
    Class compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
    //得到getPackageInfoNoCheck方法
    Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod(
            "getPackageInfoNoCheck", ApplicationInfo.class, compatibilityInfoClass);
    //生成默认的CompatibilityInfo对象
    Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
    Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);
    //生成插件的ApplicationInfo对象
    ApplicationInfo applicationInfo = PluginManager.getInstance().getApplicationInfo(component, 0);
    //生成插件的loadedApk对象
    loadedAPK = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

    /**
     * 设置插件的loadedApk的mClassLoader对象
     */
    String optimizedDirectory = PluginDirHelper.getPluginDalvikCacheDir(PluginManager.getContext(), component.getPackageName());
    String libraryPath = PluginDirHelper.getPluginNativeLibraryDir(PluginManager.getContext(), component.getPackageName());
    ClassLoader classLoader = new MyClassLoader(applicationInfo.publicSourceDir, optimizedDirectory, libraryPath, PluginManager.getContext().getClassLoader());
    Field mClassLoaderField = loadedAPK.getClass().getDeclaredField("mClassLoader");
    mClassLoaderField.setAccessible(true);
    mClassLoaderField.set(loadedAPK, classLoader);

    /**
     * 把插件的loadedApk对象插入ActivityThread的mPackages中
     */
    WeakReference weakReference = new WeakReference(loadedAPK);
    mPackages.put(component.getPackageName(), weakReference);
    sPluginLoadedApkCache.put(component.getPackageName(), loadedAPK);
} catch (Exception e) {
    e.printStackTrace();
}

生成loadedAPK对象用到的ApplicationInfo,是通过自定义的PMS生成的,我们在下面再讲。

四、插件管理

插件管理是运行在远程服务中,需要通过aidl进行通信。其实系统的PMS也是一个aidl接口,所以我们仿照系统来实现。

1.进程管理

首先是占坑,在清单文件中预注册多个代理进程和代理组件。

...








    
        
        
    
    

...

然后是进程维护。在PMS服务启动时,就查找清单文件中注册的所有的代理进程和组件。

/**
 * 初始化items
 * 通过IntentFilter,从清单文件中查找代理组件的存档信息,并添加到items中。
 * 代理组件的action统一为Intent.ACTION_MAIN,Category统一为Env.CATEGORY_ACTIVITY_PROXY_STUB。
 *
 * @param context PMS服务的上下文
 */
public void onCreate(Context context) {
    //根据代理组件的IntentFilter设置Intent
    Intent intent = new Intent(Intent.ACTION_MAIN);
    intent.addCategory(Env.CATEGORY_ACTIVITY_PROXY_STUB);
    //设置宿主APK的包名
    intent.setPackage(context.getPackageName());

    /**
     * 从宿主的PackageManager中Activity和BroadcastReceiver的存档信息
     */
    PackageManager pm = context.getPackageManager();
    List activities = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
    for (ResolveInfo activity : activities) {
        addActivityInfo(activity.activityInfo);
    }

    /**
     * 从宿主的PackageManager件中Service的存档信息
     */
    List services = pm.queryIntentServices(intent, 0);
    for (ResolveInfo service : services) {
        addServiceInfo(service.serviceInfo);
    }
    ...
}

然后,是选择代理组件。选择出可用的代理组件后,需要保存在mRunningProcessList中。

public ActivityInfo selectStubActivityInfo(int callingPid, int callingUid, ActivityInfo targetInfo) {
    //获取插件Activity的同包Activity的运行进程,如果获取到了,就代表当前插件已经开启进程了
    String stubPlugin = mRunningProcessList.getStubProcessByTarget(targetInfo);
    //如果当前插件已经开启进程了,就选择可用的代理Activity
    if (stubPlugin != null) {
        //获取插件进程在清单文件中注册的所有代理Activity
        List stunInfos = mStaticProcessList.getActivityInfoForProcessName(stubPlugin);
        for (ActivityInfo activityInfo : stunInfos) {
            //先找出与插件Activity的启动模式一致的代理Activity
            if (activityInfo.launchMode == targetInfo.launchMode) {
                //如果启动模式是Standard,就直接返回该代理Activity
                if (activityInfo.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
                    mRunningProcessList.setTargetProcessName(activityInfo, targetInfo);
                    mRunningProcessList.addActivityInfo(callingPid, callingUid, activityInfo, targetInfo);
                    return activityInfo;
                    //如果启动模式是单例,就返回没有运行的代理Activity
                } else if (!mRunningProcessList.isStubInfoUsed(activityInfo, targetInfo, stubPlugin)) {
                    mRunningProcessList.setTargetProcessName(activityInfo, targetInfo);
                    mRunningProcessList.addActivityInfo(callingPid, callingUid, activityInfo, targetInfo);
                    return activityInfo;
                }
            }
        }
        return null;
    }

    /**
     * 如果当前插件没有开启进程,就需要新开进程
     */
    //获取清单文件中注册的所有代理进程名
    List processNames = mStaticProcessList.getProcessNames();
    for (String stubProcessName : processNames) {
        //获取插件进程在清单文件中注册的所有代理Activity
        List stubInfos = mStaticProcessList.getActivityInfoForProcessName(stubProcessName);
        //如果当前进程没有运行,就设置它的目标进程名和包名,并查找可用的代理ActivityInfo
        if (!mRunningProcessList.isProcessRunning(stubProcessName)) {
            for (ActivityInfo stubInfo : stubInfos) {
                if (stubInfo.launchMode == targetInfo.launchMode) {
                    if (stubInfo.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
                        mRunningProcessList.setTargetProcessName(stubInfo, targetInfo);
                        mRunningProcessList.addActivityInfo(callingPid, callingUid, stubInfo, targetInfo);
                        return stubInfo;
                    } else if (!mRunningProcessList.isStubInfoUsed(stubInfo, targetInfo, stubProcessName)) {
                        mRunningProcessList.setTargetProcessName(stubInfo, targetInfo);
                        mRunningProcessList.addActivityInfo(callingPid, callingUid, stubInfo, targetInfo);
                        return stubInfo;
                    }
                }
            }
            //如果当前进程已经运行,但是并没有代理运行任何插件,就重新设置它的目标进程名和包名,并查找可用的代理ActivityInfo
        } else if (mRunningProcessList.isProcessRunning(stubProcessName) && mRunningProcessList.isPkgEmpty(stubProcessName)) {
            for (ActivityInfo stubInfo : stubInfos) {
                if (stubInfo.launchMode == targetInfo.launchMode) {
                    if (stubInfo.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
                        mRunningProcessList.setTargetProcessName(stubInfo, targetInfo);
                        mRunningProcessList.addActivityInfo(callingPid, callingUid, stubInfo, targetInfo);
                        return stubInfo;
                    } else if (!mRunningProcessList.isStubInfoUsed(stubInfo, targetInfo, stubProcessName)) {
                        mRunningProcessList.setTargetProcessName(stubInfo, targetInfo);
                        mRunningProcessList.addActivityInfo(callingPid, callingUid, stubInfo, targetInfo);
                        return stubInfo;
                    }
                }
            }
        }
    }
    return null;
}

2.插件APK解析

首先是安装。安装就是安装在宿主的指定目录下,自定义的PMS服务每次启动时,从指定查找所有的APK文件,就是插件APK,交给自定义的PackageParser解析。

然后是解析。使用自定义PackageParser,用来将插件APK文件转化为Pacjage对象。仿照系统的PackageParser实现,本质上是反射系统的PackageParser,实现自定义PackageParser的所有功能。 由于Android各个版本的PackageParser的现实不同,所以要做版本兼容。我们以API21为标准版本,其他版本基于API21做修改。

解析其实是交给系统来做,我们要做的是通过反射来调用系统方法,实现PackageParser的所有方法。

//生成插件APK的包信息
public PackageInfo generatePackageInfo(
        int gids[], int flags, long firstInstallTime, long lastUpdateTime,
        HashSet grantedPermissions) throws Exception {
    /*public static PackageInfo generatePackageInfo(PackageParser.Package p,
        int gids[], int flags, long firstInstallTime, long lastUpdateTime,
        HashSet grantedPermissions, PackageUserState state, int userId) */
    try {
        Method method = MethodUtils.getAccessibleMethod(sPackageParserClass, "generatePackageInfo",
                mPackage.getClass(),
                int[].class, int.class, long.class, long.class, Set.class, sPackageUserStateClass, int.class);
        return (PackageInfo) method.invoke(null, mPackage, gids, flags, firstInstallTime, lastUpdateTime, grantedPermissions, mDefaultPackageUserState, mUserId);
    } catch (NoSuchMethodException e) {
        Log.i(TAG, "get generatePackageInfo 1 fail", e);
    }
    ...
 }

最后

代码地址:https://gitee.com/yanhuo2008/AndroidCommon/tree/master/ToolPluggable

喜欢请点赞,谢谢!

你可能感兴趣的:(安卓移动架构08-手写DroidPlugin插件化框架)