移动架构08-手写DroidPlugin插件化框架
一、插件化介绍
插件化就是一种让插件(第三方的APP)运行在宿主(自己的APP)中的技术。
插件本身是一个APP,安装以后就可以运行在Android系统中。使用插件化技术,不会把插件安装在Android系统中,而是安装在宿主中,由宿主管理所有的插件,相当于插件是宿主的一个模块。
插件化的好处:
- 集成海量插件。插件是第三方开发的,我们只需要遵循一定的规范,把插件集成到宿主中,就可以把插件当成自己的模块使用。
- 安全。插件是由第三方开发的,如果出现崩溃,不会导致宿主APP崩溃。因为每个插件都是运行在独立的进程中。
- 减小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。
步骤如下:
- Hook第二步,也就是Hook系统的ActivityManager。将Activity替换成代理Activity,并保存真实的Activity。
- Hook第四步,也就是Hook当前ActivityThread的mH。将代理Activity替换成真实的Activity。
然后,我们要加载插件的class文件。思路是,先将插件APK生成loadedApk对象,然后插入ActivityThread.mPackages中,然后将ApplicaitonInfo的包名修改为插件的包名,然后获取插件的loadedApk对象来创建Activity。
步骤如下:
- Hook第四步,也就是Hook当前ActivityThread的mH。将ApplicaitonInfo的包名修改为插件的包名
- Hook第五步,也就是Hook当前ActivityThread的sPackageManager。将PackageManager.getPackageInfo()的返回值改为null。
- Hook第六步,也就是Hook系统的ClassLoader对象。将插件APK的loadedApk对象插入ActivityThread.mPackages中。
2.插件管理
插件管理做两件事:1.进程管理;2.插件APK解析。
2.1进程管理
一个插件运行在宿主中,为了不想宿主和其它插件的运行,需要运行在单独的代理进程中。一个Activity有四种启动模式,如果是单例的启动模式,就需要多个相同启动模式的代理Activity,否则就不能同时打开多个单例的插件Activity。
所以,进程管理就是选择合适的代理进程和代理组件。实现分为两步:占坑和进程维护。
首先是占坑。占坑就是在清单文件中注册多个代理进程和代理Activity,并且区分进程。
步骤如下:
- 在清单文件中注册多个进程的Activity,Standard的代理Activity只要1个,其他模式的代理Activity需要多个。
- 为了识别代理进程,进程名统一以PluginP开头。为了识别代理组件,使用统一的action和category。
- 在清单文件中,预注册多个权限。避免插件APP得不到权限,而导致崩溃。
然后是进程维护。进程维护就是管理所有的代理进程和代理组件,为插件提供可用的代理组件。
步骤如下:
- 通过action和category查询所有的代理进程和组件,保存在StaticProcessList中;
- 维护正在运行的进程和组件。通过代理进程名标识一个正在运行的进程,通过包名表示代理进程运行的插件,将所有正在运行的代理进程和组件,保存在RunningProcessList中。
- 选择代理组件。打开插件组件时,需要选择代理组件。选择代理组件有三个原则:1.相同插件的组件使用相同的进程;2.插件组件的启动模式要和代理组件的一致;3.单例的代理组件只能代理运行一个插件组件。
2.2插件APK解析
插件APK解析分为两步:1.安装;2.解析。
系统使用PMS服务来安装、解析APK文件,我们模仿系统使用一个自定义的PMS来安装、解析APK文件。
首先是安装。安装就是插件APK文件放到指定目录下,我们统一放到/data/user/0/宿主包名/Plugin/插件包名/apk/下。
然后是解析。解析就是获取插件APK的包信息和注册的组件信息等。使用自定义的PackageParser来解析,需要做版本兼容。
下面,就说一下怎么具体实现。
三、添加Hook点
Hook点有4个:
- HookActivityManager:Hook系统的ActivityManager;
- HookMH:Hook当前ActivityThread的mH;
- HookPackageManager:Hook当前ActivityThread的sPackageManager;
- HookClassLoader:Hook系统的ClassLoader对象
Hook就是拦截系统消息,替换执行内容。所以我们把拦截和替换分开来说。
1.HookActivityManager
首先,Hook系统的ActivityManager,拦截它的startActivity(),将跳转目标设为代理组件,从而跳过PMS检查。
步骤如下:
- 通过反射获取系统的ActivityManagerNative的gDefault属性;
- 通过反射gDefault,获取IactivityManager属性
- 使用动态代理的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的远程服务,用来管理插件的安装。进程名以PluginP开头。-->
<service
android:name=".pm.MPackageManagerService"
android:process=":PluginService" />
<!--使用占坑的方式,加载插件APK的组件。即预注册多个代理组件,供插件使用。
由于Activity有四种启动模式,所以需要注册多个进程多种模式的代理组件。
设置代理组件的进程名时,统一以PluginP开头。
代理组件,统一设置category为"gsw.toolpluggable.plugin",方便解析插件时判断是否是插件的组件。-->
<!--第1个进程-->
<activity
android:name=".activity.ActivityMode$P01$Standard01"
android:allowTaskReparenting="true"
android:excludeFromRecents="true"
android:exported="false"
android:hardwareAccelerated="true"
android:launchMode="standard"
android:process=":PluginP01">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="gsw.toolpluggable.plugin" />
</intent-filter>
<meta-data
android:name="com.morgoo.droidplugin.ACTIVITY_STUB_INDEX"
android:value="1" />
</activity>
...
然后是进程维护。在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<ResolveInfo> activities = pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
for (ResolveInfo activity : activities) {
addActivityInfo(activity.activityInfo);
}
/**
* 从宿主的PackageManager件中Service的存档信息
*/
List<ResolveInfo> 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<ActivityInfo> 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<String> processNames = mStaticProcessList.getProcessNames();
for (String stubProcessName : processNames) {
//获取插件进程在清单文件中注册的所有代理Activity
List<ActivityInfo> 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<String> grantedPermissions) throws Exception {
/*public static PackageInfo generatePackageInfo(PackageParser.Package p,
int gids[], int flags, long firstInstallTime, long lastUpdateTime,
HashSet<String> 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/Common
移动架构专题:https://www.jianshu.com/nb/25128604
喜欢请点赞,谢谢!
网友评论