背景
在研究sdk插件化热更新方式的过程中总结出了两套插件资源加载方案,在此记录下
资源热更方式
方式一:合并所有插件资源
需要解决资源id冲突问题
资源ID值一共4个字段,由三部分组成:PackageId+TypeId+EntryId
- PackageId:是包的Id值,Android 中如果第三方应用的话,这个默认值是 0x7f,系统应用的话就是 0x01 ,插件的话那么就是给插件分配的id值,占用1个字节。
- TypeId:是资源的类型Id值,一般 Android 中有这几个类型:attr,drawable,layout,anim,raw,dimen,string,bool,style,integer,array,color,id,menu 等。【应用程序所有模块中的资源类型名称,按照字母排序之后。值是从1开支逐渐递增的,而且顺序不能改变(每个模块下的R文件的相同资源类型id值相同)。比如:anim=0x01占用1个字节,那么在这个编译出的所有R文件中anim 的值都是 0x01】
- EntryId:是在具体的类型下资源实例的id值,从0开始,依次递增,他占用2个字节。
两种解决方式
- gradle3.5以上可以动态配置aapt的package-id参数修改插件apk的packageId,可在插件模块的build.gradle配置如下:
android {
compileSdkVersion 32
defaultConfig {
applicationId "xxx"
minSdkVersion 21
targetSdkVersion 32
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//为模块定一个唯一的package-id,如0x80
aaptOptions {
additionalParameters '--allow-reserved-package-id','--package-id', '0x80'
}
}
- gradle3.5以下,可以将打包出来的插件apk包进行解包,修改public.xml文件内资源id的package-id,再扫描每一个R$xxx.smali文件,纠正代码中R类的值,与public.xml中的对应,重新打包。
反射替换Application中的mResources字段
注意:在启动service和receiver的时候,会使用application的resource
fun mergePatchResources(application: Application, apkPaths : List<String>) {
val newAssetManagerObj = AssetManager::class.java.newInstance()
//todo 需要注意这里的addAssetPath被标志为废弃了
val addAssetPath = AssetManager::class.java.getMethod("addAssetPath", String::class.java)
// 插入宿主的资源
addAssetPath.invoke(newAssetManagerObj, application.baseContext.packageResourcePath)
// 插入插件的资源
apkPaths.forEach {
addAssetPath.invoke(newAssetManagerObj, it)
}
val newResourcesObj = Resources(
newAssetManagerObj,
application.baseContext.resources.displayMetrics,
application.baseContext.resources.configuration
)
newHoldResources = newResourcesObj
val resourcesField = application.baseContext.javaClass.getDeclaredField("mResources")
resourcesField.isAccessible = true
resourcesField[application.baseContext] = newResourcesObj
val packageInfoField = application.baseContext.javaClass.getDeclaredField("mPackageInfo")
packageInfoField.isAccessible = true
val packageInfoObj = packageInfoField[application.baseContext]
// 获取 mPackageInfo 变量对象中类的Resources类型的mResources 变量,并替换它的值为新的Resources对象
// 注意:这是最主要的需要替换的,如果不需要支持插件运行时更新,只留这一个就可以了
val resourcesField2 = packageInfoObj.javaClass.getDeclaredField("mResources")
resourcesField2.isAccessible = true
resourcesField2[packageInfoObj] = newResourcesObj
// 获取 ContextImpl 中的 Resources.Theme 类型的 mTheme 变量,并至空它
// 注意:清理mTheme对象,否则通过inflate方式加载资源会报错, 如果是activity动态加载插件,则需要把activity的mTheme对象也设置为null
val themeField = application.baseContext.javaClass.getDeclaredField("mTheme")
themeField.isAccessible = true
themeField[application.baseContext] = null
}
更新Activity的资源
- 监听activity的onCreate回调
((Application)context).registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
Log.d("zbm111", "doMonitorActivity---onCreate");
Resources resources = "含宿主和所有插件资源的完整resource对象"
ResHookUtil.monkeyPatchExistingResources(activity, resources);
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
});
- 将要启动的activity反射替换mResources字段
public static void monkeyPatchExistingResources(Activity activity, Resources newResources) {
try {
Class<?> contextThemeWrapperClass = null;
try {
contextThemeWrapperClass = Class.forName("android.view.ContextThemeWrapper");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 反射获取 ContextThemeWrapper 类的 mResources 字段
Field mResourcesField = null;
try {
mResourcesField = contextThemeWrapperClass.getDeclaredField("mResources");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
// 设置字段可见性
mResourcesField.setAccessible(true);
// 将插件资源设置到插件 Activity 中
try {
mResourcesField.set(activity, newResources);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Method mTheme = contextThemeWrapperClass.getDeclaredMethod("setTheme", Resources.Theme.class);
mTheme.setAccessible(true);
mTheme.invoke(activity, (Object) null);
} catch (Exception e) {
e.printStackTrace();
}
}
方式二:封装含插件资源的Resource对象
注意:插件apk的包名须与宿主的包名保持一致
需要解决资源id冲突问题
参考方式一,可以通过修改type id与宿主区分开
hook ActivityThread的handler设置callback
- 给ActivityThread中mH(Hander类型)对象的mCallback字段设置一个代理对象ProxyHandlerCallback
public static void doHandlerHook(Context context) {
try {
HookUtils.context = context;
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object activityThread = currentActivityThread.invoke(null);
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, new ProxyHandlerCallback(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
- 系统启动service前,进行拦截,将ActivityThread的mPackages新增一个service的LoadedApk,该LoadedApk的mResources字段替换为含插件+宿主资源的SingleMixRes对象;
系统启动receiver前,进行拦截,将application的mResources字段替换为含插件+宿主资源的SingleMixRes对象。
public class ProxyHandlerCallback implements Handler.Callback {
private Handler mBaseHandler;
private Map<String, String> mPathToPluginNameMap = new HashMap<>();
public ProxyHandlerCallback(Handler mBaseHandler) {
this.mBaseHandler = mBaseHandler;
}
@Override
public boolean handleMessage(Message msg) {
Log.d("zbm111", "接受到消息了msg:" + msg);
if (msg.what == 113){
//启动receiver的时候走这里
try {
Object object = msg.obj;
Field infoField = object.getClass().getDeclaredField("info");
infoField.setAccessible(true);
ActivityInfo activityInfo = (ActivityInfo) infoField.get(object);
String hostReceiverName = activityInfo.name;
Resources resources = "含插件+宿主资源的SingleMixRes对象";
Field resourcesField = ((Application)HookUtils.getContext()).getBaseContext().getClass().getDeclaredField("mResources");
resourcesField.setAccessible(true);
resourcesField.set(((Application)HookUtils.getContext()).getBaseContext(), resources);
}catch (Exception e){
Log.e("zbm111", "handle create receiver failed");
}
} else if (msg.what == 114) {
//启动service的时候走这里
try {
Object object = msg.obj;
Field infoField = object.getClass().getDeclaredField("info");
infoField.setAccessible(true);
ServiceInfo serviceInfo = (ServiceInfo) infoField.get(object);
String hostServiceName = serviceInfo.name;
String path = "插件apk本地存储路径"
//todo dex热修复必须同时进行资源热更
if (path != null) {
replaceLoadApk(hostServiceName, path, false);
Log.i("zbm111", "replaced to plugin service success");
}
} catch (Exception e) {
Log.e("zbm111", "handle create service failed");
}
}
mBaseHandler.handleMessage(msg);
return true;
}
private void replaceLoadApk(String componentName, String path, boolean isUseActivity) throws Exception {
Log.i("zbm111", "start replaceLoadApk");
Field activityThreadField = Class.forName("android.app.ActivityThread").getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object sCurrentActivityThread = activityThreadField.get(null);
Field mPackagesField = sCurrentActivityThread.getClass().getDeclaredField("mPackages");
mPackagesField.setAccessible(true);
ArrayMap mPackages = (ArrayMap) mPackagesField.get(sCurrentActivityThread);
if (null == mPackages) {
Log.i("zbm111", "can not get mPackages");
return;
}
ApplicationInfo applicationInfo = generateApplicationInfo(path);
if (null != applicationInfo) {
Field compatibilityInfoField = Class.forName("android.content.res.CompatibilityInfo").getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
compatibilityInfoField.setAccessible(true);
Object defaultCompatibilityInfo = compatibilityInfoField.get(null);
Object loadedApk;
if (isUseActivity){
Method getPackageInfoMethod = sCurrentActivityThread.getClass().getDeclaredMethod("getPackageInfo", ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo"), int.class);
getPackageInfoMethod.setAccessible(true);
loadedApk = getPackageInfoMethod.invoke(sCurrentActivityThread, applicationInfo, defaultCompatibilityInfo, Context.CONTEXT_INCLUDE_CODE);
}else {
Method getPackageInfoMethod = sCurrentActivityThread.getClass().getDeclaredMethod("getPackageInfoNoCheck", ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo"));
getPackageInfoMethod.setAccessible(true);
loadedApk = getPackageInfoMethod.invoke(sCurrentActivityThread, applicationInfo, defaultCompatibilityInfo);
}
String pluginName = applicationInfo.packageName;
if (!TextUtils.isEmpty(pluginName)) {
Log.i("zbm111", "plugin pkg name is " + pluginName);
Resources resources = "含插件+宿主资源的SingleMixRes对象";
setResource(loadedApk, resources);
mPackages.put(pluginName, new WeakReference<>(loadedApk));
mPackagesField.set(sCurrentActivityThread, mPackages);
mPathToPluginNameMap.put(path, pluginName);
} else {
Log.i("zbm111", "get plugin pkg name failed");
}
} else {
Log.i("zbm111", "can not get application info");
}
}
private void setResource(Object loadedApk, Resources resources) throws Exception{
Field mResourcesField = loadedApk.getClass().getDeclaredField("mResources");
mResourcesField.setAccessible(true);
mResourcesField.set(loadedApk, resources);
}
public ApplicationInfo generateApplicationInfo(String pluginPath) {
try {
ApplicationInfo applicationInfo = getApplicationInfoByPackageArchiveInfo(pluginPath);
if (null == applicationInfo) {
LogUtil.i("zbm111", "get applicationInfo failed");
return null;
}
applicationInfo.sourceDir = pluginPath;
applicationInfo.publicSourceDir = pluginPath;
return applicationInfo;
} catch (Exception e) {
LogUtil.i("zbm111", "generateApplicationzInfo failed " + e.getMessage());
}
return null;
}
private ApplicationInfo getApplicationInfoByPackageArchiveInfo(String pluginPath) {
PackageManager packageManager = HookUtils.getContext().getPackageManager();
if (null == packageManager) {
LogUtil.i("zbm111", "get PackageManager failed");
return null;
}
PackageInfo packageInfo = packageManager.getPackageArchiveInfo(pluginPath, 0);
if (null == packageInfo) {
LogUtil.i("zbm111", "get packageInfo failed");
return null;
}
return packageInfo.applicationInfo;
}
}
- 自定义ResourcesWrapper
public class ResourcesWrapper extends Resources {
private Resources mBase;
public ResourcesWrapper(Resources base){
super(base.getAssets(),base.getDisplayMetrics(),base.getConfiguration());
mBase = base;
}
@Override
public CharSequence getText(int id) throws NotFoundException {
return mBase.getText(id);
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public Typeface getFont(int id) throws NotFoundException {
return mBase.getFont(id);
}
@Override
public CharSequence getQuantityText(int id, int quantity) throws NotFoundException {
return mBase.getQuantityText(id, quantity);
}
@Override
public String getString(int id) throws NotFoundException {
return mBase.getString(id);
}
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
return mBase.getString(id, formatArgs);
}
@Override
public String getQuantityString(int id, int quantity, Object... formatArgs) throws NotFoundException {
return mBase.getQuantityString(id, quantity, formatArgs);
}
@Override
public String getQuantityString(int id, int quantity) throws NotFoundException {
return mBase.getQuantityString(id, quantity);
}
@Override
public CharSequence getText(int id, CharSequence def) {
return mBase.getText(id, def);
}
@Override
public CharSequence[] getTextArray(int id) throws NotFoundException {
return mBase.getTextArray(id);
}
@Override
public String[] getStringArray(int id) throws NotFoundException {
return mBase.getStringArray(id);
}
@Override
public int[] getIntArray(int id) throws NotFoundException {
return mBase.getIntArray(id);
}
@Override
public TypedArray obtainTypedArray(int id) throws NotFoundException {
return mBase.obtainTypedArray(id);
}
@Override
public float getDimension(int id) throws NotFoundException {
return mBase.getDimension(id);
}
@Override
public int getDimensionPixelOffset(int id) throws NotFoundException {
return mBase.getDimensionPixelOffset(id);
}
@Override
public int getDimensionPixelSize(int id) throws NotFoundException {
return mBase.getDimensionPixelSize(id);
}
@Override
public float getFraction(int id, int base, int pbase) {
return mBase.getFraction(id, base, pbase);
}
@Override
public Drawable getDrawable(int id) throws NotFoundException {
return mBase.getDrawable(id);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(int id, Theme theme) throws NotFoundException {
return mBase.getDrawable(id, theme);
}
@Override
public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return mBase.getDrawableForDensity(id, density);
} else {
return null;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(int id, int density, Theme theme) {
return mBase.getDrawableForDensity(id, density, theme);
}
@Override
public Movie getMovie(int id) throws NotFoundException {
return mBase.getMovie(id);
}
@Override
public int getColor(int id) throws NotFoundException {
return mBase.getColor(id);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public int getColor(int id, Theme theme) throws NotFoundException {
return mBase.getColor(id, theme);
}
@Override
public ColorStateList getColorStateList(int id) throws NotFoundException {
return mBase.getColorStateList(id);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {
return mBase.getColorStateList(id, theme);
}
@Override
public boolean getBoolean(int id) throws NotFoundException {
return mBase.getBoolean(id);
}
@Override
public int getInteger(int id) throws NotFoundException {
return mBase.getInteger(id);
}
@Override
public XmlResourceParser getLayout(int id) throws NotFoundException {
return mBase.getLayout(id);
}
@Override
public XmlResourceParser getAnimation(int id) throws NotFoundException {
return mBase.getAnimation(id);
}
@Override
public XmlResourceParser getXml(int id) throws NotFoundException {
return mBase.getXml(id);
}
@Override
public InputStream openRawResource(int id) throws NotFoundException {
return mBase.openRawResource(id);
}
@Override
public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
return mBase.openRawResource(id, value);
}
@Override
public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
return mBase.openRawResourceFd(id);
}
@Override
public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
mBase.getValue(id, outValue, resolveRefs);
}
@Override
public void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
mBase.getValueForDensity(id, density, outValue, resolveRefs);
}
}
@Override
public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
mBase.getValue(name, outValue, resolveRefs);
}
@Override
public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
return mBase.obtainAttributes(set, attrs);
}
@Override
public DisplayMetrics getDisplayMetrics() {
return mBase.getDisplayMetrics();
}
@Override
public Configuration getConfiguration() {
return mBase.getConfiguration();
}
@Override
public int getIdentifier(String name, String defType, String defPackage) {
return mBase.getIdentifier(name, defType, defPackage);
}
@Override
public String getResourceName(int resid) throws NotFoundException {
return mBase.getResourceName(resid);
}
@Override
public String getResourcePackageName(int resid) throws NotFoundException {
return mBase.getResourcePackageName(resid);
}
@Override
public String getResourceTypeName(int resid) throws NotFoundException {
return mBase.getResourceTypeName(resid);
}
@Override
public String getResourceEntryName(int resid) throws NotFoundException {
return mBase.getResourceEntryName(resid);
}
@Override
public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws XmlPullParserException, IOException {
mBase.parseBundleExtras(parser, outBundle);
}
@Override
public void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException {
mBase.parseBundleExtra(tagName, attrs, outBundle);
}
}
- SingleMixRes优先从插件加载资源,找不到则从宿主加载资源
public class SingleMixRes extends ResourcesWrapper{
private Resources mHostResources;
public SingleMixRes(Resources hostResources, Resources pluginResources) {
super(pluginResources);
mHostResources = hostResources;
}
@Override
public CharSequence getText(int id) throws NotFoundException {
try {
return super.getText(id);
} catch (NotFoundException e) {
return mHostResources.getText(id);
}
}
@Override
public String getString(int id) throws NotFoundException {
try {
return super.getString(id);
} catch (NotFoundException e) {
return mHostResources.getString(id);
}
}
@Override
public String getString(int id, Object... formatArgs) throws NotFoundException {
try {
return super.getString(id,formatArgs);
} catch (NotFoundException e) {
return mHostResources.getString(id,formatArgs);
}
}
@Override
public float getDimension(int id) throws NotFoundException {
try {
return super.getDimension(id);
} catch (NotFoundException e) {
return mHostResources.getDimension(id);
}
}
@Override
public int getDimensionPixelOffset(int id) throws NotFoundException {
try {
return super.getDimensionPixelOffset(id);
} catch (NotFoundException e) {
return mHostResources.getDimensionPixelOffset(id);
}
}
@Override
public int getDimensionPixelSize(int id) throws NotFoundException {
try {
return super.getDimensionPixelSize(id);
} catch (NotFoundException e) {
return mHostResources.getDimensionPixelSize(id);
}
}
@Override
public Drawable getDrawable(int id) throws NotFoundException {
try {
return super.getDrawable(id);
} catch (NotFoundException e) {
return mHostResources.getDrawable(id);
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawable(int id, Theme theme) throws NotFoundException {
try {
return super.getDrawable(id, theme);
} catch (NotFoundException e) {
return mHostResources.getDrawable(id,theme);
}
}
@Override
public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {
try {
return super.getDrawableForDensity(id, density);
} catch (NotFoundException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
return mHostResources.getDrawableForDensity(id, density);
} else {
return null;
}
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public Drawable getDrawableForDensity(int id, int density, Theme theme) {
try {
return super.getDrawableForDensity(id, density, theme);
} catch (Exception e) {
return mHostResources.getDrawableForDensity(id,density,theme);
}
}
@Override
public int getColor(int id) throws NotFoundException {
try {
return super.getColor(id);
} catch (NotFoundException e) {
return mHostResources.getColor(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public int getColor(int id, Theme theme) throws NotFoundException {
try {
return super.getColor(id,theme);
} catch (NotFoundException e) {
return mHostResources.getColor(id,theme);
}
}
@Override
public ColorStateList getColorStateList(int id) throws NotFoundException {
try {
return super.getColorStateList(id);
} catch (NotFoundException e) {
return mHostResources.getColorStateList(id);
}
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public ColorStateList getColorStateList(int id, Theme theme) throws NotFoundException {
try {
return super.getColorStateList(id,theme);
} catch (NotFoundException e) {
return mHostResources.getColorStateList(id,theme);
}
}
@Override
public boolean getBoolean(int id) throws NotFoundException {
try {
return super.getBoolean(id);
} catch (NotFoundException e) {
return mHostResources.getBoolean(id);
}
}
@Override
public XmlResourceParser getLayout(int id) throws NotFoundException {
try {
return super.getLayout(id);
} catch (NotFoundException e) {
return mHostResources.getLayout(id);
}
}
@Override
public String getResourceName(int resid) throws NotFoundException {
try {
return super.getResourceName(resid);
} catch (NotFoundException e) {
return mHostResources.getResourceName(resid);
}
}
@Override
public int getInteger(int id) throws NotFoundException {
try {
return super.getInteger(id);
} catch (NotFoundException e) {
return mHostResources.getInteger(id);
}
}
@Override
public CharSequence getText(int id, CharSequence def) {
try {
return super.getText(id,def);
} catch (NotFoundException e) {
return mHostResources.getText(id,def);
}
}
@Override
public InputStream openRawResource(int id) throws NotFoundException {
try {
return super.openRawResource(id);
} catch (NotFoundException e) {
return mHostResources.openRawResource(id);
}
}
@Override
public XmlResourceParser getXml(int id) throws NotFoundException {
try {
return super.getXml(id);
} catch (NotFoundException e) {
return mHostResources.getXml(id);
}
}
@TargetApi(Build.VERSION_CODES.O)
@Override
public Typeface getFont(int id) throws NotFoundException {
try {
return super.getFont(id);
} catch (NotFoundException e) {
return mHostResources.getFont(id);
}
}
@Override
public Movie getMovie(int id) throws NotFoundException {
try {
return super.getMovie(id);
} catch (NotFoundException e) {
return mHostResources.getMovie(id);
}
}
@Override
public XmlResourceParser getAnimation(int id) throws NotFoundException {
try {
return super.getAnimation(id);
} catch (NotFoundException e) {
return mHostResources.getAnimation(id);
}
}
@Override
public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
try {
return super.openRawResource(id,value);
} catch (NotFoundException e) {
return mHostResources.openRawResource(id,value);
}
}
@Override
public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
try {
return super.openRawResourceFd(id);
} catch (NotFoundException e) {
return mHostResources.openRawResourceFd(id);
}
}
}
更新Activity的资源
参考方式一
两种方式注意事项
- 打包出的插件apk需要去除第三方smali代码,否则可能会报资源问题
- 代码更新与资源更新最好同步,预防id对应不上
附录
修改资源id冲突及去除第三方smali文件工具