1.背景
在进行插件解耦化开发时,为了减轻宿主APP的负担,会适当将某些定制性Feature的代码剥离出去,改为以另外一个apk/jar的形式定制化集成. 本质上该部分代码还是需要运行在宿主APP进程中. 有时候需要用到宿主APP外的Context上下文.
2.简单用法
简单用法如下:
Context pluginContext = context.createPackageContext(包名, Context.CONTEXT_INCLUDE_CODE);
但是遗憾的是会抛出SecurityException, 原因是不推荐这种做法,让一个外包的代码插入到一个宿主apk里边来执行,有很大的安全风险.
2.1 运行报错
报错log如下:
07-04 15:27:13.166 7507 7507 D PreFragment: java.lang.SecurityException: Requesting code from com.xsq.testdemo (with uid 10191) to be run in process com.start.testdemo (with uid 10185)
07-04 15:27:13.166 7507 7507 D PreFragment: at android.app.ActivityThread.getPackageInfo(ActivityThread.java:2866)
07-04 15:27:13.166 7507 7507 D PreFragment: at android.app.ActivityThread.getPackageInfo(ActivityThread.java:2840)
07-04 15:27:13.166 7507 7507 D PreFragment: at android.app.ContextImpl.createPackageContextAsUser(ContextImpl.java:2745)
07-04 15:27:13.166 7507 7507 D PreFragment: at android.app.ContextImpl.createPackageContext(ContextImpl.java:2730)
07-04 15:27:13.166 7507 7507 D PreFragment: at android.content.ContextWrapper.createPackageContext(ContextWrapper.java:1092)
07-04 15:27:13.166 7507 7507 D PreFragment: at android.content.ContextWrapper.createPackageContext(ContextWrapper.java:1092)
07-04 15:27:13.166 7507 7507 D PreFragment: at com.start.testdemo.utils.Utils.testPathClassLoader(Utils.java:208)
2.2报错代码追踪
2.2.1 API声明
/Sdk/sources/android-33/android/content/Context.java
/**
* Return a new Context object for the given application name. This
* Context is the same as what the named application gets when it is
* launched, containing the same resources and class loader. Each call to
* this method returns a new instance of a Context object; Context objects
* are not shared, however they share common state (Resources, ClassLoader,
* etc) so the Context instance itself is fairly lightweight.
*
* <p>Throws {@link android.content.pm.PackageManager.NameNotFoundException} if there is no
* application with the given package name.
*
* <p>Throws {@link java.lang.SecurityException} if the Context requested
* can not be loaded into the caller's process for security reasons (see
* {@link #CONTEXT_INCLUDE_CODE} for more information}.
*
* @param packageName Name of the application's package.
* @param flags Option flags.
*
* @return A {@link Context} for the application.
*
* @throws SecurityException
* @throws PackageManager.NameNotFoundException if there is no application with
* the given package name.
*/
public abstract Context createPackageContext(String packageName,
@CreatePackageOptions int flags) throws PackageManager.NameNotFoundException;
/**
* Flag for use with {@link #createPackageContext}: include the application
* code with the context. This means loading code into the caller's
* process, so that {@link #getClassLoader()} can be used to instantiate
* the application's classes. Setting this flags imposes security
* restrictions on what application context you can access; if the
* requested application can not be safely loaded into your process,
* java.lang.SecurityException will be thrown. If this flag is not set,
* there will be no restrictions on the packages that can be loaded,
* but {@link #getClassLoader} will always return the default system
* class loader.
*/
public static final int CONTEXT_INCLUDE_CODE = 0x00000001;
/**
* Flag for use with {@link #createPackageContext}: ignore any security
* restrictions on the Context being requested, allowing it to always
* be loaded. For use with {@link #CONTEXT_INCLUDE_CODE} to allow code
* to be loaded into a process even when it isn't safe to do so. Use
* with extreme care!
*/
public static final int CONTEXT_IGNORE_SECURITY = 0x00000002;
2.2.2 API实现端
实现端在ContextImpl中,代码如下:
/Sdk/sources/android-33/android/app/ContextImpl.java
@Override
public Context createPackageContext(String packageName, int flags)
throws NameNotFoundException {
return createPackageContextAsUser(packageName, flags, mUser);
}
@Override
public Context createPackageContextAsUser(String packageName, int flags, UserHandle user)
throws NameNotFoundException {
if (packageName.equals("system") || packageName.equals("android")) {
// The system resources are loaded in every application, so we can safely copy
// the context without reloading Resources.
return new ContextImpl(this, mMainThread, mPackageInfo, mParams,
mAttributionSource.getAttributionTag(),
mAttributionSource.getNext(),
null, mToken, user, flags, null, null);
}
LoadedApk pi = mMainThread.getPackageInfo(packageName, mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE, user.getIdentifier()); // 关键行
if (pi != null) {
ContextImpl c = new ContextImpl(this, mMainThread, pi, mParams,
mAttributionSource.getAttributionTag(),
mAttributionSource.getNext(),
null, mToken, user, flags, null, null);
final int displayId = getDisplayId();
final Integer overrideDisplayId = mForceDisplayOverrideInResources
? displayId : null;
c.setResources(createResources(mToken, pi, null, overrideDisplayId, null,
getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
if (c.mResources != null) {
return c;
}
}
// Should be a better exception.
throw new PackageManager.NameNotFoundException(
"Application package " + packageName + " not found");
}
根据log栈,抛出异常的关键在于上述ActivityThread的getPackageInfo方法内部.
/Sdk/sources/android-33/android/app/ActivityThread.java
public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
int flags, int userId) {
final boolean differentUser = (UserHandle.myUserId() != userId);
ApplicationInfo ai = PackageManager.getApplicationInfoAsUserCached(
packageName,
PackageManager.GET_SHARED_LIBRARY_FILES
| PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
(userId < 0) ? UserHandle.myUserId() : userId);
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (differentUser) {
// Caching not supported across users
ref = null;
} else if ((flags & Context.CONTEXT_INCLUDE_CODE) != 0) {
ref = mPackages.get(packageName);
} else {
ref = mResourcePackages.get(packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (ai != null && packageInfo != null) {
if (!isLoadedApkResourceDirsUpToDate(packageInfo, ai)) {
List<String> oldPaths = new ArrayList<>();
LoadedApk.makePaths(this, ai, oldPaths);
packageInfo.updateApplicationInfo(ai, oldPaths);
}
if (packageInfo.isSecurityViolation() // 关键条件
&& (flags&Context.CONTEXT_IGNORE_SECURITY) == 0) {
throw new SecurityException(
"Requesting code from " + packageName
+ " to be run in process "
+ mBoundApplication.processName
+ "/" + mBoundApplication.appInfo.uid);
}
return packageInfo;
}
}
if (ai != null) {
return getPackageInfo(ai, compatInfo, flags);
}
return null;
}
从上可知,关键中的关键是一个isSecurityViolation()的判断,决定了是否抛出异常,终止执行.
这就取决于packageInfo(LoadedApk)的构造和初始化流程了.
还是ActivityThread中initInstrumentation()方法有看到new LoadedApk()不过其默认是false.
private void initInstrumentation(
InstrumentationInfo ii, AppBindData data, ContextImpl appContext) {
ApplicationInfo instrApp;
try {
instrApp = getPackageManager().getApplicationInfo(ii.packageName, 0,
UserHandle.myUserId());
} catch (RemoteException e) {
instrApp = null;
}
if (instrApp == null) {
instrApp = new ApplicationInfo();
}
ii.copyTo(instrApp);
instrApp.initForUser(UserHandle.myUserId());
final LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
appContext.getClassLoader(), /*securityViolation*/false, /*includeCode*/true, /*registerPackage*/false);
//省略若干
}
经过查找, 定位到如下方法会将默认为false的violation更新为true.即触发抛出安全异常的条件.
@UnsupportedAppUsage(trackingBug = 171933273)
public final LoadedApk getPackageInfo(ApplicationInfo ai, CompatibilityInfo compatInfo,
int flags) {
boolean includeCode = (flags&Context.CONTEXT_INCLUDE_CODE) != 0;
boolean securityViolation = includeCode && ai.uid != 0
&& ai.uid != Process.SYSTEM_UID && (mBoundApplication != null
? !UserHandle.isSameApp(ai.uid, mBoundApplication.appInfo.uid)
: true); // 关键决定Violation状态的逻辑
boolean registerPackage = includeCode && (flags&Context.CONTEXT_REGISTER_PACKAGE) != 0;
if ((flags&(Context.CONTEXT_INCLUDE_CODE
|Context.CONTEXT_IGNORE_SECURITY))
== Context.CONTEXT_INCLUDE_CODE) {
if (securityViolation) {
String msg = "Requesting code from " + ai.packageName
+ " (with uid " + ai.uid + ")";
if (mBoundApplication != null) {
msg = msg + " to be run in process "
+ mBoundApplication.processName + " (with uid "
+ mBoundApplication.appInfo.uid + ")";
}
throw new SecurityException(msg);
}
}
return getPackageInfo(ai, compatInfo, null, securityViolation, includeCode,
registerPackage);
}
条件信息如下:
1.设置了Context.CONTEXT_INCLUDE_CODE flag.
2.不是uid不为空且uid不为system uid;
3.两个app的uid不相同,则会为true,导致触发抛出安全异常.
2.3 解决方案
根据以上代码追踪,可能解决方案如下:
1) 直接调用API时,将Context.CONTEXT_IGNORE_SECURITY和Context.CONTEXT_INCLUDE_CODE一并传入; 这也是网上大批帖子直接教demo时推荐的方案.
不过这个异常抛出本身是因为安全问题, 正式项目是不推荐的.除非宿主APP在扫描时加上签名验证.
2) 将两个app共享uid, 清单文件中设置相同的android:sharedUserId值,并签上相同的签名给予保护,同时在利用PackageManager.扫描目标apk中的plugin实现时,最好要进行APP签名确认.
==> 这个方式比较推荐.
2.4 android:sharedUserId被废弃
在开发和文档中明确指出,该项在API 29就已经被废弃了(但是目前依然是可用状态,实测Android14之前(含)可用).
官方文档说明如下:
https://developer.android.google.cn/guide/topics/manifest/manifest-element?hl=zh-cn
根据文档说明,后续可能还会直接直接移除该标签.在此过渡期间,还特意在Adnroid 13中加入了android:sharedMaxSdkVersion标签.
如果结合该标签,则会在新平台里边,直接忽略sharedUserId标签.
因此,在Context中又找到来一个hide API, createApplicationContext(ApplicationInfo info, int flags).
2.5 createApplicationContext API
声明如下:
/**
* Creates a context given an {@link android.content.pm.ApplicationInfo}.
*
* @hide
*/
@SuppressWarnings("HiddenAbstractMethod")
@UnsupportedAppUsage
public abstract Context createApplicationContext(ApplicationInfo application,
@CreatePackageOptions int flags) throws PackageManager.NameNotFoundException;
/Sdk/sources/android-33/android/app/ContextImpl.java
@Override
public Context createApplicationContext(ApplicationInfo application, int flags)
throws NameNotFoundException {
LoadedApk pi = mMainThread.getPackageInfo(application, mResources.getCompatibilityInfo(),
flags | CONTEXT_REGISTER_PACKAGE);
if (pi != null) {
ContextImpl c = new ContextImpl(this, mMainThread, pi, ContextParams.EMPTY,
mAttributionSource.getAttributionTag(),
mAttributionSource.getNext(),
null, mToken, new UserHandle(UserHandle.getUserId(application.uid)),
flags, null, null);
final int displayId = getDisplayId();
final Integer overrideDisplayId = mForceDisplayOverrideInResources
? displayId : null;
c.setResources(createResources(mToken, pi, null, overrideDisplayId, null,
getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
if (c.mResources != null) {
return c;
}
}
throw new PackageManager.NameNotFoundException(
"Application package " + application.packageName + " not found");
}
从上可知,其并无LoadedApk的限制. 本方案适合OEM平台APP开发.
使用示例:
ApplicationInfo appInfo = pm.getApplicationInfo(pkgName,0);
Log.d(TAG, "gotPlugin: applicationInfo:" + appInfo);
Context context = Utils.createApplicationContext(getActivity(),appInfo,0);
Log.d(TAG, "gotPlugin: got applicationCtx ;" + context + " ,ctxapplicationInfo:" + context.getApplicationInfo().className);
// 反射方式访问hide API
public static Context createApplicationContext(Activity context, ApplicationInfo appInfo, int flags) {
Context applicationCtx = null;
Class<?> claz = null;
try {
claz = context.getClass().getSuperclass();//Class.forName("android.content.Context");
Method method = claz.getMethod("createApplicationContext", ApplicationInfo.class, int.class);
applicationCtx = (Context) method.invoke(context, appInfo, flags);
} catch (NullPointerException | NoSuchMethodException | IllegalAccessException |
InvocationTargetException e) {
e.printStackTrace();
}
return applicationCtx;
}
3.总结
1.createPackageContext API 使用时可以是宿主和插件APP shareUserId, 或者是在签名保护的基础上加上Context.CONTEXT_IGNORE_SECURITY.
2.createApplicationContext 隐藏API无需共享uid, 更加适合平台APP使用,三方APP不建议使用.