Context的createPackageContext用法

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 &nbsp;
     * @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不建议使用.

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值