痛点分析
App的国家支持多,端侧在打包时多语言资源已经确定,而多语言配置变更频繁,准确性要求高,运营同学配置容易出错,特别是大促期间,版本发布冻结,为了能够及时修复线上的多语言问题,需要有动态更新多语言的能力。
简单来说,就是 修改多语言 String.xml中string文案,每次都需要发版本,这样效率不高。
<string name="my_gift_card_search_sort_prompt">Search By</string>
方案实现
实现原理:
通过AssetManager 异步载入资源包,构建新的Resource对象,并设置给全局单例对象GlobalResourceManager。
资源获取时调用 单例GlobalResourceManager.getString(resId);
步骤:
1.构建Patch资源apk
新建独立项目:新建一个独立Android App 应用。
配置修复资源:在对应 src/hotfix_resource/res/values 下面配置同名已经修复的string资源,
构建资源包:通过 ./gradlew assembleRelease 构建生成对应apk,最终生成后重命名为.patch。
资源包只包含需要修改的 string资源。
2.App下载资源包
可以利用App现有的能力,下载资源包到本地。(可以通过服务端提供资源下载地址;或者App自有的获取Orange配置等方式下载)
3.加载资源包
下载完成之后,需要完成有效性校验。 通过 AssetsManager 异步载入资源包,构建新的Resource对象,并设置给全局单例对象GlobalResourceManager。
public Resources loadHotPatchResource() {
PackageManager pm = context.getPackageManager();
PackageInfo mInfo = pm.getPackageArchiveInfo(hotfixResFilePath, PackageManager.GET_ACTIVITIES);
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, hotfixResFilePath);
Resources originRes = mResourcesManager.getDefaultResources();
return new Resources(assetManager, originRes.getDisplayMetrics(), originRes.getConfiguration());
}
4.获取资源方式
GlobalResourceManager.getString(resId);
可以进行封装,如 kotlin扩展函数:
fun Context.getResourceString(@StringRes resId: Int):String {
return GlobalResourceManager.getString(this, resId)
}
fun Activity.getResourceString(@StringRes resId: Int):String {
return GlobalResourceManager.getString(this, resId)
}
fun Fragment.getResourceString(@StringRes resId: Int):String {
return GlobalResourceManager.getString(this, resId)
}
fun View.getResourceString(@StringRes resId: Int):String {
return GlobalResourceManager.getString(context, resId)
}
fun RecyclerView.ViewHolder.getResourceString(@StringRes resId: Int):String {
return GlobalResourceManager.getString(itemView.context, resId)
}
GlobalResouceManager
private fun getString(resString: String?, @StringRes resId: Int):String {
var string = resString ?: "";
try {
if (ResourcePatchLoader.getPatchResource() != null && ResourcePatchLoader.getPatchResourcePackageName() != null) {
val resName = ApplicationContext.getContext().resources.getResourceEntryName(resId)
val patchResId = ResourcePatchLoader.getPatchResource()?.getIdentifier(
resName, "string", ResourcePatchLoader.getPatchResourcePackageName()) ?: 0
if (patchResId == 0) {
return string
}
string = ResourcePatchLoader.getPatchResource()?.getString(patchResId) ?: ""
}
} catch (e: Throwable) {
if (ResourcePatchDebug.isDebug()) {
throw Resources.NotFoundException("String resource ID #0x" + Integer.toHexString(resId))
} else {
Logger.d(TAG_RESOURCE_HOT_PATCH, "GlobalResourceManager getString fail: " + e.message)
PatchTrackUtil.onFail(POINT_GET_RESOURCE_STRING, 100, e.message)
}
}
return string
}