目录
概述
大家在做游戏切包时,可能都会遇到上图这种资源找不到导致崩溃的问题,本文将全面而详细地分析在处理游戏切包时,关于资源合并的问题。
问题分析
原理
在切包时,我们一般是将游戏母包和sdk包两个apk合并,在本文中我们称游戏母包为基础包,称sdk包为扩展包
我们在切包时,基本流程如上,目的是为了将扩展包的内容合并到基础包中,达到更新代码的目的,因此主要流程就是
- 反编译得到包体内容
- 合并扩展包内容到基础包中,本文主要探讨res目录
- 回编译得到新的apk
分析
那么为什么会出现resources$notfoundexception
异常呢?
该异常表明指定id对应的资源不存在,可是资源明明都已经合并,并且回编译进新apk了,为什么会说资源不存在?
那是因为在android中,我们是根据资源id,在resources.arsc
中查找对应资源的,资源都已经回编译进新apk,那么在resources.arsc
中应该是确实存在这个资源信息的(我们可以通过androd studio查看验证),那么问题就是出在资源id上了。
那为什么资源在基础包合扩展包中分别访问都正常,合并后会不正常?
这是因为在编译基础包和扩展包时,它们的资源是独立编译的,在产生r.java
时,即使是双方共有的资源,资源id也可能不一样。如下图:
假设基础包和扩展包中都包含hello_world
这个字符串资源
在基础包中,这个资源对应的资源id是0x7f050001
,而在扩展包中,对应的资源id是0x7f0f0013
在合并回编译后,这个资源id确实还存在于apk中,但是资源id变成了0x7f050033
如果我们不做任何处理,那么在新包里面,来自扩展包的代码在获取hello_world
时还是会使用0x7f0f0013
去获取这个资源,那么此时要么会获取到错误的资源,要么就会报resources$notfoundexception
异常了。
解决思路
根据上面分析,我们已经知道关键问题在于资源id映射错误上,那么我们只要确保回编译后,资源id映射关系是正确的,就可以解决这个问题了。如下图:
如果我们可以
- 保持新包的资源id和基础包一样,那么基础包的代码就不会出问题
- 将扩展包的资源id修改成新包的资源id,那么扩展包的代码也不会出问题了
接下来,我们就向这两点努力~
行动
0x01:保留旧id
要做到保留旧id,我们要解决2个问题
- 怎样获取所有旧id
- 怎样在回编译时复用旧id
获取旧id:public.xml
获取旧id方法很简单,apk通过apktool
反编译后,我们可以找到res/values/public.xml
,这个文件包含了apk所有的资源id,内容一般如下:
可以看到,其中包含了public
标签,我们只需要解析xml,就可以获取到所有资源的类型,名称和资源id的值。
复用旧id
我们在合并扩展包的资源到基础包后,新增的资源并没有分配资源id,因为基础包的public.xml
中不存在扩展包的资源,所以我们要对合并后,新增的资源分配资源id。
大部分博客提到的方式是自己根据资源,手动在public.xml
中添加资源,并且递增资源id实现。
本文使用aapt2
处理。
aapt2命令
关键为在link
资源时使用--stable-ids
来指定资源id。
该命令接受一个特定格式的文件,如下:
com.demo.res:anim/abc_fade_in = 0x7f010000 com.demo.res:animator/design_appbar_state_list_animator = 0x7f020000 com.demo.res:array/tddispresetproperties = 0x7f030000 com.demo.res:attr/sharedvalue = 0x7f040000 com.demo.res:bool/abc_action_bar_embed_tabs = 0x7f050000 com.demo.res:color/abc_background_cache_hint_selector_material_dark = 0x7f060000 com.demo.res:dimen/abc_action_bar_content_inset_material = 0x7f070000 com.demo.res:drawable/$avd_hide_password__0 = 0x7f080000 com.demo.res:id/alt = 0x7f090000 com.demo.res:integer/abc_config_activitydefaultdur = 0x7f0a0000 com.demo.res:layout/abc_action_bar_title_item = 0x7f0c0000 com.demo.res:menu/menu_openchat_info = 0x7f0d0000 com.demo.res:mipmap/app_icon = 0x7f0e0000 com.demo.res:raw/firebase_common_keep = 0x7f0f0000 com.demo.res:string/freeformwindoworientation_landscape = 0x7f100000 com.demo.res:style/alertdialog.appcompat = 0x7f110000 com.demo.res:xml/appsflyer_backup_rules = 0x7f130000
很容易看到,内容格式为package:type/name = value
,我们只需要把上面获取到的public.xml
中的所有资源转换成这个格式,就可以用来输入到aapt2 link
命令的--stable-ids
参数中,那么新编译的资源,只要public.xml
中存在,那么资源id的值就不会变化,另外aapt2
也会自动给新增的资源分配一个合适的资源id。
保留新资源id
那么我们怎样得到新的public.xml
呢,这里要在使用link
命令时使用--emit-ids
来指定资源id的输出目录,那么我们就可以得到一个格式同上的文件,里面包含的所有参与编译的资源的资源id。
然后我们再将这个文件的内容,转换为public.xml
,再放进新包的res/values
下。
至此,我们已经成功做到
- 保留了旧的资源id
- 对新增的资源分配了资源id
- 获取到了合并后的所有资源的资源id
0x02:修改资源id
因为我们保留了基础包的资源id,那么对于基础包的代码来说,资源没有任何变化,那么就不需要修改基础包的资源id了,那么我们接下来要处理扩展包的资源id,把用到的资源id改为新的值。
tips:扩展包一般是sdk包,我们可以使用
resources#getidentifier
来通过资源名称来动态获取资源id来规避扩展包中资源id不正确的问题。在实践中是可以了,不过会给代码维护带来一些问题。
需要修改的位置
资源id本质上是r.java
中的静态变量,在实际代码中,我们也是通过引用的方式使用资源id的,而r.java
在反编译后,会变成smali文件,因此我们需要处理的就是扩展包中所有r.smali
的资源id。
特别地,由于app
模块在编译的时候有可能会对静态变量进行编译优化,所以实际上,其他smali文件,例如activity
的smali文件中,会直接使用资源id常量值的情况。
但是在游戏切包场景下,sdk一般都是以库的形式存在,对于library
模块,因为资源id在编译时不是常量,所以并不会出现编译优化的情况。而对于游戏母包,因为我们复用了游戏母包(基础包)的资源id,游戏母包的资源id没有发生变化,那么即使它使用了常量值也不会有问题。
更新r smali
那么接下来我们就对r smali文件进行修改,按照上述分析,我们只需要对来自扩展包的r文件进行更新即可。
普通id
对于一般的r文件,结构如下:
.class public final landroidx/activity/r$id; .super ljava/lang/object; # annotations // .... # static fields .field public static final view_tree_on_back_pressed_dispatcher_owner:i = 0x7f0902df # direct methods .method private constructor()v // ... .end method
可以看到,r文件内部就是一系列的static fields
,我们通过文件名称确定资源类型,如r$id.smali
保存的是id
资源,通过.field public static final name:i = value
来获取资源的名称(name
)和当前值(value
)。
在上面我们已经获取到新的public.xml
,可以获取到所有资源的资源id,那么我们只需要匹配资源的类型和名称,然后用新的值替换当前值就可以了。
r$styleable.smali
对于r$styleable.smali
,情况有点不同。
r$styleable.smali
的内容是通过
生成的,每个
对应一个attr
数组和数个下标值。例如:
对应
.field public static final colorstatelistitem:[i .field public static final colorstatelistitem_alpha:i = 0x3 .field public static final colorstatelistitem_android_alpha:i = 0x1 .field public static final colorstatelistitem_android_color:i = 0x0 .field public static final colorstatelistitem_android_lstar:i = 0x2 .field public static final colorstatelistitem_lstar:i = 0x4
对于下标,我们可以忽略,因为只要数组大小和顺序不变,下标也不需要变。
那么我们需要修改数组内的值,但是我们可以发现,数组的内容并没有和定义放在一起,那么我们要怎样处理?
实际上,我们可以忽略数组定义,因为
并没有新增资源id,数组内的资源id都是attr
资源id,所以我们要做的就是:
- 找到当前文件的旧的资源id,找到它对应的
attr
资源名称,我们通过解析扩展包的public.xml
就可以获取到旧的资源id对应的名称。 - 再通过资源名称找到它在新包中的资源id值,通过上面保存的新的资源id很容易可以做到。
- 替换,这一步需要注意不能全局替换,逐行遍历替换就可以了。
其他细节
系统资源
在资源合并的时候我们有时候会看到一些特别的资源id,例如0x101011c
,这类资源id为系统资源,实际上我们可以忽略这部分资源id。也可以在中找到这些资源id
aapt2输出r.java
除了使用--emit-ids
可以获取到所有资源id之外,也可以选择使用--java
来输出r.java
,同样可以获取到所有资源id
aapt2编译时报系统资源找不到
需要使用比apk编译版本高的android.jar
总结
希望对大家有帮助,欢迎在评论区一起交流~
以上就是一文带你搞清楚android游戏发行切包资源id那点事的详细内容,更多关于android 游戏发行切包资源id的资料请关注其它相关文章!