插件化基础(二)——加载插件资源

本文详细探讨了Android应用中插件化资源的加载原理,包括Resources和AssetManager的角色,以及如何通过反射加载插件资源。文章区分了独立式和合并式两种加载方式,并分析了资源冲突的原因及解决方案,特别是针对资源ID冲突和AppCompatActivity启动异常的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

系列文章目录:

插件化基础(一)——加载插件的类
插件化基础(二)——加载插件资源
插件化基础(三)——启动插件组件


一、了解 Asset 和 Resources

我们加载的资源通常来自 res 和 assets 两个目录,加载它们的方式有所不同:

	// 使用 Resources 获取 res 目录下的资源
	String appName = getResources().getString(R.string.app_name);
	
	// 使用 AssetManager 获取 assets 目录下的资源
	InputStream inputStream = getAssets().open("xxx.png");

表面上看起来似乎是不同的加载方式,但查看源码,会发现二者实际上都是通过 AssetManager 去执行资源加载的。就比如说通过 Resources 获取字符串的过程:

	public String getString(@StringRes int id) throws NotFoundException {
        return getText(id).toString();
    }
	
	public CharSequence getText(@StringRes int id) throws NotFoundException {
		// ResourcesImpl.getAssets() 获取到 AssetManager,再去获取字符串
        CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
        if (res != null) {
            return res;
        }
        throw new NotFoundException("String resource ID #0x"
                + Integer.toHexString(id));
    }

内部实际上会先拿到 ResourcesImpl 中的 AssetManager 再去获取相应资源。

既然都是用 AssetManager 去加载资源,那两种加载资源的方式有何区别呢?

  • Resources 主要用来访问被编译过的应用资源文件,在访问这些文件之前,会先根据资源 ID 查找得到对应的资源文件名
  • AssetManager 既可以通过文件名访问那些被编译过的,也可以访问没有被编译过的资源文件

两种加载方式的不同其实也映射出 res 和 assets 两个资源目录的差别:

  • res:系统会为 res 目录下的所有资源文件生成一个 ID,这意味着很容易就可以访问到这个资源,甚至在 xml 中都是可以访问的,使用 ID 访问的速度是最快的
  • assets:不会生成 ID,只能通过 AssetManager 访问,xml 中不能访问,访问速度会慢些,不过操作更加方便

二、系统是如何加载资源的

了解宿主加载资源的方式,会给我们加载插件资源提供灵感。

在上一节中,我们提到 Resources 内部都会有一个 AssetManager 来真正执行资源的加载,而 Resources 又会与 Context 绑定,所以了解 Context 是如何创建 Resources 对象的就成为了关键。由于 Application 与四大组件都有类似的过程,限于篇幅,我们以 Activity 为例看看源码是怎么做的。

先上时序图,结合源码更易理解:

系统在启动一个 Activity 时会调用 handleLaunchActivity(),由 performLaunchActivity() 返回一个 Activity 对象,接着在 createBaseContextForActivity() 中创建 Activity 的 Context:

	private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
        final int displayId;
        try {
            displayId = ActivityManager.getService().getActivityDisplayId(r.token);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
		
		// 创建 Activity 的 Context
        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);

        ...
        return appContext;
    }

ContextImpl 的 createActivityContext() 来负责 Context 的创建工作:

	static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
            Configuration overrideConfiguration) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");

        String[] splitDirs = packageInfo.getSplitResDirs();
        ClassLoader classLoader = packageInfo.getClassLoader();

        if (packageInfo.getApplicationInfo().requestsIsolatedSplitLoading()) {
            try {
                classLoader = packageInfo.getSplitClassLoader(activityInfo.splitName);
                splitDirs = packageInfo.getSplitPaths(activityInfo.splitName);
            } catch (NameNotFoundException e) {
                ...
            } finally {
                ...
            }
        }

		// 1.先创建一个 ContextImpl 对象
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
                activityToken, null, 0, classLoader);

        // Clamp display ID to DEFAULT_DISPLAY if it is INVALID_DISPLAY.
        displayId = (displayId != Display.INVALID_DISPLAY) ? displayId : Display.DEFAULT_DISPLAY;

        final CompatibilityInfo compatInfo = (displayId == Display.DEFAULT_DISPLAY)
                ? packageInfo.getCompatibilityInfo()
                : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;

        final ResourcesManager resourcesManager = ResourcesManager.getInstance();

        // 2.创建一个基础 Resources 对象,该 Activity 的所有配置上下文都会依赖于这个 Resources
        context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                packageInfo.getResDir(),
                splitDirs,
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                classLoader));
        context.mDisplay = resourcesManager.getAdjustedDisplay(displayId,
                context.getResources());
        return context;
    }

通过 ResourcesManager 的 createBaseActivityResources() 创建一个 Resources 对象:

	public @Nullable Resources createBaseActivityResources(@NonNull IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
        	...
        	// resDir 是资源路径,可以为 null(则只加载 framework 中的资源)
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();

            synchronized (this) {
                // Force the creation of an ActivityResourcesStruct.
                getOrCreateActivityResourcesStructLocked(activityToken);
            }

            // Update any existing Activity Resources references.
            updateResourcesForActivity(activityToken, overrideConfig, displayId,
                    false /* movedToDifferentDisplay */);

            // 根据 key 请求一个真实的 Resources 对象,名字上能判断出并不是每次都创建一个新的 Resources
            return getOrCreateResources(activityToken, key, classLoader);
        } ...
    }

getOrCreateResources() 会根据给定的 ResourcesKey 是否存在,而决定创建一个新的 Resources 还是直接返回一个已经缓存过的 Resources 对象:

	// ResourcesImpl 的缓存
	private final ArrayMap<ResourcesKey, WeakReference<ResourcesImpl>> mResourceImpls =
            new ArrayMap<>();

	private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        // 1.同步块中会根据 key 去 mResourceImpls 缓存中查找是否有对应的 Resources,有就直接返回
        synchronized (this) {
            if (activityToken != null) {
                final ActivityResources activityResources =
                        getOrCreateActivityResourcesStructLocked(activityToken);

                ...

                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }
            } else {
                ...

                // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }
            }
        }

        // 2.代码运行到这里,说明没能根据 key 找到 Resources,所以就创建一个
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        if (resourcesImpl == null) {
            return null;
        }

        synchronized (this) {
            ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
            if (existingResourcesImpl != null) {
                
                resourcesImpl.getAssets().close();
                resourcesImpl = existingResourcesImpl;
            } else {
                // 将这个 resourcesImpl 放入缓存
                mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
            }

            final Resources resources;
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
            return resources;
        }
    }

在第 2 步调用 createResourcesImpl() 创建一个 ResourcesImpl 时,会先通过 createAssetManager() 创建一个 AssetManager 对象,并将其与新创建的 ResourcesImpl 绑定:

	private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

		// 注意创建 AssetManager 时要传入 ResourcesKey
        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        // 将 assets 与 impl 绑定
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

        return impl;
    }

在用 createAssetManager() 创建 AssetManager 时,会检查传入的 ResourcesKey 中的各种资源路径,如果非空,AssetManager 就会通过 addAssetPath() 向 AssetManager 添加这些额外的资源:

	protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                return null;
            }
        }

        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }

        if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }

        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return assets;
    }

addAssetPath() 内最终会调用到 native 方法 addAssetPathNative() 去执行添加操作:

	public final int addAssetPath(String path) {
        return  addAssetPathInternal(path, false);
    }
	
	private final int addAssetPathInternal(String path, boolean appAsLib) {
        synchronized (this) {
            int res = addAssetPathNative(path, appAsLib);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

	private native final int addAssetPathNative(String path, boolean appAsLib);

看到这里,实现插件资源加载的思路也就出现了:

  1. 需要创建一个 Resources 对象,用来加载插件的资源
  2. Resources 的创建需要 AssetManager 对象
  3. AssetManager 创建的时候,需要指定资源路径

那么我们通过反射 AssetManager 的 addAssetPath(),将插件路径添加进去就可以了。

三、加载插件资源的方式

第一篇文章在讲加载插件类的时候介绍了两种方式:单 DexClassLoader 和多 DexClassLoader,区别在于一个 DexClassLoader 对象是只负责加载一个插件,还是负责加载所有插件。

资源加载也是如此的分成两种方式:

  1. 独立式:专门创建 AssetManager、Resources 加载插件资源(一个 AssetManager 只负责加载一个插件)
  2. 合并式:插件资源和宿主资源直接合并(一个 AssetManager 加载宿主与所有插件)

下面我们分别来介绍这两种方式的实现方法。

3.1 独立式

正如我们前面说的那样,反射得到 AssetManager 对象,并在调用 addAssetPath() 时将插件路径传递进去即可:

	public Resources loadResource(Context context, String pluginPath) {
        if (TextUtils.isEmpty(pluginPath) || !new File(pluginPath).exists()) {
            Log.e(TAG, "插件包路径有误!");
            return null;
        }
        try {
            // 获取 AssetManager 并执行 addAssetPath() 将插件路径传递进去
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPathMethod.invoke(assetManager, pluginPath);

            // 创建一个绑定 assetManager 的 Resources 对象并返回
            Resources resources = context.getResources();
            return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }

仅有以上代码并不意味着大功告成,还需要考虑一个问题,由于宿主与各个插件间的 Resources 是隔离的,当前不能相互访问资源,这该如何解决?

如果将 loadResource() 放在宿主的 Application 中:

public class MyApplication extends Application {

    private Resources mResources;

    @Override
    public void onCreate() {
        super.onCreate();

        mResources = PluginManager.getInstance().loadResource(this, "XXX");
    }

    @Override
    public Resources getResources() {
        return mResources == null ? super.getResources() : mResources;
    }
}

然后在插件的 BaseActivity 中从 Application 获取 Resources:

	@Override
    public Resources getResources() {
        if (getApplication() != null && getApplication().getResources() != null) {
            return getApplication().getResources();
        }
        return super.getResources();
    }

这样做虽然确实能在宿主中加载到插件的资源,但是有两个问题:

  1. 宿主的 Application 中的 getResources() 返回的是插件的 Resources,而不是宿主的,这种做法影响到了宿主资源的加载
  2. 插件的 Application 不会被执行(由于双亲委派,只能加载宿主的 Application)

鉴于以上两点,loadResource() 放在宿主中不可行,那就尝试放在插件中:

public class LoadUtil {

    private static final String TAG = LoadUtil.class.getSimpleName();

    private static volatile Resources sResources;

    public static Resources getResources(Context context, String pluginPath) {
        if (sResources == null) {
            synchronized (LoadUtil.class) {
                if (sResources == null) {
                    sResources = loadResource(context, pluginPath);
                }
            }
        }
        return sResources;
    }

    public static Resources loadResource(Context context, String pluginPath) {
        if (TextUtils.isEmpty(pluginPath) || !new File(pluginPath).exists()) {
            Log.e(TAG, "插件包路径有误!");
            return null;
        }
        try {
            // 获取 AssetManager 并执行 addAssetPath() 将插件路径传递进去
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPathMethod.invoke(assetManager, pluginPath);

            // 创建一个绑定 assetManager 的 Resources 对象并返回,注意这个 context 不能是 Activity
            Resources resources = context.getResources();
            return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

插件的 BaseActivity 在调用 getResources() 时,传递的 Context 应该是 Application 的,不能是 Activity 的:

	@Override
    public Resources getResources() {
        Resources resources = LoadUtil.getResources(getApplication(), PLUGIN_APK_PATH);
        // 插件作为单独 app 时需要返回 super.getResources()
        return resources == null ? super.getResources() : resources;
    }

因为假如 loadResource() 的 context 是 Activity,那么就会执行到 BaseActivity 的 getResources(),进而执行 LoadUtil.getResources(),形成循环调用,导致程序崩溃。

综上来看,独立式虽然实现了资源隔离,不会在单个 Resources 对象中发生资源冲突(注意有强调单个哦,因为多个 Resources 间可能会发生相同资源 ID 表示不同资源文件的情况,下一节会介绍),但是资源共享的过程比较麻烦。

3.2 合并式

合并式需要在执行 addAssetPath() 时将宿主与所有插件的资源路径全部加进去,实现方式与独立式的 loadResource() 很像,只不过需要一个 Resources 对象加载所有插件的资源路径:

	public Resources loadResources(Context context, List<String> pluginPaths) {
        if (pluginPaths == null || pluginPaths.size() == 0) {
            throw new IllegalArgumentException("插件集合不能拿为空!");
        }

        try {
            // 获取 AssetManager 并执行 addAssetPath() 将插件路径传递进去
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
            for (String pluginPath : pluginPaths) {
                File pluginFile = new File(pluginPath);
                if (!pluginFile.exists()) {
                    Log.e(TAG, "插件文件不存在:" + pluginPath);
                    continue;
                }
                addAssetPathMethod.invoke(assetManager, pluginPath);
            }
            // 创建一个绑定 assetManager 的 Resources 对象并返回
            Resources resources = context.getResources();
            return new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        return null;
    }

这样做的好处是宿主与插件能直接相互访问资源,但也有一个明显的缺点是,在 Resources 内,宿主与插件可能会因为生成相同的资源 ID 而发生资源冲突。

四、资源冲突

不论使用哪种方式,都可能会产生资源冲突,原因是 apk 在编译打包的过程中会为资源生成资源 ID,而宿主与各个插件都是单独打包编译的,就难免会产生不同 apk 间的资源 ID 相同,但实际引用的资源文件不同的情况。

比如说宿主中一个 layout 资源 ID 为 0x7f0b0070:

同样的资源 ID,在插件中表示的就是另一个 layout 文件:

在介绍资源冲突的解决方法之前,先来了解资源 ID 的生成过程。

4.1 资源 ID 的生成

在编译打包 apk 时,资源会被 aapt 工具处理,生成 R.java 文件和 .ap_ 文件。

aapt 工具生成资源 ID 的时序图:

该工具源码在 /frameworks/base/tools/aapt/ 目录下,入口在 main.cpp 的 main():

int main(int argc, char* const argv[])
{
    Bundle bundle;
    ...

	if (argv[1][0] == 'v')
        bundle.setCommand(kCommandVersion);
    else if (argv[1][0] == 'p')
    	// 注意编译资源时会使用这个 kCommandPackage 的命令,后面会用到
        bundle.setCommand(kCommandPackage);
    // 省略其它的命令判断...
    
    bundle.setFileSpec(argv, argc);
	// 重点是这里,命令处理
    result = handleCommand(&bundle);
    return result;
}

handleCommand() 会根据不同的命令调用相应方法:

int handleCommand(Bundle* bundle)
{
    switch (bundle->getCommand()) {
    case kCommandVersion:      return doVersion(bundle);
    case kCommandList:         return doList(bundle);
    case kCommandDump:         return doDump(bundle);
    case kCommandAdd:          return doAdd(bundle);
    case kCommandRemove:       return doRemove(bundle);
    // 针对包进行处理
    case kCommandPackage:      return doPackage(bundle);
    case kCommandCrunch:       return doCrunch(bundle);
    case kCommandSingleCrunch: return doSingleCrunch(bundle);
    case kCommandDaemon:       return runInDaemonMode(bundle);
    default:
        fprintf(stderr, "%s: requested command not yet supported\n", gProgName);
        return 1;
    }
}

编译资源时会调用 Command.cpp 中的 doPackage():

int doPackage(Bundle* bundle)
{
	if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
		// 去 Resource.cpp,编译资源
        err = buildResources(bundle, assets, builder);
        if (err != 0) {
            goto bail;
        }
    }
}

然后创建资源表 ResourceTable:

status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
	ResourceTable table(bundle, String16(assets->getPackage()), packageType);
}

ResourceTable 的构造函数会根据包的类型决定 packageId:

ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type)
    : mAssetsPackage(assetsPackage)
    , mPackageType(type)
    , mTypeIdOffset(0)
    , mNumLocal(0)
    , mBundle(bundle)
{
    ssize_t packageId = -1;
    switch (mPackageType) {
    	// app 的 packageId 为 0x7f
        case App:
        case AppFeature:
            packageId = 0x7f;
            break;
		// 系统资源 ID 就是以 0x01 开头
        case System:
            packageId = 0x01;
            break;
        case SharedLibrary:
            packageId = 0x00;
            break;
        default:
            assert(0);
            break;
    }
    sp<Package> package = new Package(mAssetsPackage, packageId);
    mPackages.add(assetsPackage, package);
    mOrderedPackages.add(package);

    // Every resource table always has one first entry, the bag attributes.
    const SourcePos unknown(String8("????"), 0);
    getType(mAssetsPackage, String16("attr"), unknown);
}

这个 packageId 其实就是 apk 包中 resources.arsc 文件中资源 ID 的前两位:

资源 ID 由十六进制数字表示,由三部分组成:

  • PackageId:apk 包的 id,默认为 0x7f
  • TypeId:资源类型 id,如 layout、id、string 等都有自己的类型 id 值,从 0x01 开始按顺序递增,如 attr = 0x01,drawable = 0x02
  • EntryId:TypeId 下各个资源的 id 值,从 0x0000 开始递增

正是因为我们编译的 app 的资源 ID 固定由 0x7f 开头,再加上 TypeId、EntryId 也有固定的取值范围,所以不同 apk 间发生资源冲突的概率是很大的。

4.2 解决

知道了资源冲突的原因,解决方法也就出现了,无非就是将插件资源的 PackageId 修改成其它未被系统占用的值,如 0x70~0x7e,修改方式有三种:

  1. 修改 aapt 工具源码,在编译期间就进行修改,参考文章Android中如何修改编译的资源ID值

  2. 修改 aapt 工具的产物,在编译后期重新整理插件的资源,编排 ID,参考文章插件化-解决插件资源ID与宿主资源ID冲突的问题

  3. build.gradle 中配置 aaptOptions(只在 compileSdkVersion ≥ 28 时才生效):

    android {
        aaptOptions {
            additionalParameters  "--package-id", "0x66","--allow-reserved-package-id"
        }
        ...
    }
    

此外,独立式还可能会发生因为双亲委派机制而产生的资源冲突,当宿主与插件都使用 AppCompatActivity(demo 使用的 appcompat 版本是 1.3.0)时可能会发生如下异常:

	java.lang.RuntimeException: Unable to start activity ComponentInfo{com.demo.hook.plugin/com.demo.hook.plugin.PluginActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2817)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2892)
        ...
    Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
        at androidx.appcompat.app.AppCompatDelegateImpl.createSubDecor(AppCompatDelegateImpl.java:903)
        at androidx.appcompat.app.AppCompatDelegateImpl.ensureSubDecor(AppCompatDelegateImpl.java:809)
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:696)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:195)
        at com.demo.hook.plugin.PluginActivity.onCreate(PluginActivity.java:11)
        ...

源码是 AppCompatDelegateImpl 的 createSubDecor() 出现空指针:

	private ViewGroup createSubDecor() {
		...

		// decor_content_parent 获取失败导致 mDecorContentParent 为 null
		mDecorContentParent = (DecorContentParent) subDecor
               .findViewById(R.id.decor_content_parent);
         mDecorContentParent.setWindowCallback(getWindowCallback());
        ...
	}

获取 decor_content_parent 失败的根本原因是,这个 id 在宿主和插件中的资源 ID 值不同:

宿主

插件

decor_content_parent 在宿主中的资源 ID 为 0x7f08007c,在插件中为 0x7f08007d,又因为类加载机制不会重复加载已经加载过的类(包名类名相同的类),所以即使是插件中的 AppCompatDelegateImpl 运行的也是宿主的代码,所以 findViewById(R.id.decor_content_parent) 带入的是宿主的 0x7f08007c,但是在插件的资源中找到的就不是 decor_content_parent,而是 decelerateAndComplete(在上图中),获取失败导致空指针。

如果测试时宿主和插件的 decor_content_parent 资源 ID 一样导致无法复现出该问题,可以在宿主或者插件的 layout 中通过 @+id/xxx 的方式生成一个资源 ID,由于资源 ID 是根据资源名称字母顺序排列的,所以 xxx 取一个字母顺序在 decor 之前的就可以了。

这类问题的根本原因就是在加载插件资源时,使用了宿主的 Context 加载了宿主的资源。解决方法是,在插件中自定义一个属于插件的 Context 并且绑定加载插件资源的 Resources:

public class BaseActivity extends AppCompatActivity {

    private static final String PLUGIN_APK_PATH = "/data/data/com.demo.hook.host/files/hook_plugin-debug.apk";
    protected Context mContext;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Resources resources = LoadUtil.getResources(getApplicationContext(), PLUGIN_APK_PATH);
        mContext = new ContextThemeWrapper(getBaseContext(), 0);
        // 替换 ContextImpl 中的 mResources
        Class<? extends Context> clazz = mContext.getClass();
        try {
            Field mResourcesField = clazz.getDeclaredField("mResources");
            mResourcesField.setAccessible(true);
            mResourcesField.set(mContext, resources);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Resources getResources() {
        /* 这种方式也不行,有冲突,改在 onCreate() 中实现
        // 插件没有 Application,所以 getApplicationContext()/getApplication() 拿到的都是宿主的
        Resources resources = LoadUtil.getResources(getApplicationContext(), PLUGIN_APK_PATH);
        // 插件作为单独 app 时需要返回 super.getResources()
        return resources == null ? super.getResources() : resources;*/
        return super.getResources();
    }
}

然后需要显示的 Activity 通过 mContext 获取 View:

public class PluginActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View view = LayoutInflater.from(mContext).inflate(R.layout.activity_plugin, null);
        setContentView(view);
    }
}

这样就避免空指针异常正常加载了:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值