当前位置 : 主页 > 手机开发 > android >

一文带你搞清楚Android游戏发行切包资源ID那点事

来源:互联网 收集:自由互联 发布时间:2023-05-16
目录 概述 问题分析 原理 分析 解决思路 行动 0x01:保留旧ID 获取旧ID:public.xml 复用旧ID Aapt2命令 保留新资源ID 0x02:修改资源ID 需要修改的位置 更新R Smali 普通ID R$styleable.smali 其他细节
目录
  • 概述
  • 问题分析
    • 原理
  • 分析
    • 解决思路
  • 行动
    • 0x01:保留旧ID
      • 获取旧ID:public.xml
    • 复用旧ID
      • Aapt2命令
    • 保留新资源ID
    • 0x02:修改资源ID
      • 需要修改的位置
        • 更新R Smali
          • 普通ID
          • R$styleable.smali
      • 其他细节
        • 系统资源
          • aapt2输出R.java
            • aapt2编译时报系统资源找不到
            • 总结

              概述

              大家在做游戏切包时,可能都会遇到上图这种资源找不到导致崩溃的问题,本文将全面而详细地分析在处理游戏切包时,关于资源合并的问题。

              问题分析

              原理

              在切包时,我们一般是将游戏母包和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,内容一般如下:

              <?xml version="1.0" encoding="utf-8"?>
              <resources>
                  <public type="anim" name="abc_fade_in" id="0x7f010000"/>
                  <public type="animator" name="design_appbar_state_list_animator" id="0x7f020000"/>
                  <public type="array" name="TDDisPresetProperties" id="0x7f030000"/>
                  <public type="attr" name="SharedValue" id="0x7f040000"/>
                  <public type="bool" name="abc_action_bar_embed_tabs" id="0x7f050000"/>
                  <public type="color" name="abc_background_cache_hint_selector_material_dark" id="0x7f060000"/>
                  <public type="dimen" name="abc_action_bar_content_inset_material" id="0x7f070000"/>
                  <public type="drawable" name="$avd_hide_password__0" id="0x7f080000"/>
                  <public type="id" name="ALT" id="0x7f090000"/>
                  <public type="integer" name="abc_config_activityDefaultDur" id="0x7f0a0000"/>
                  <public type="interpolator" name="btn_checkbox_checked_mtrl_animation_interpolator_0" id="0x7f0b0000"/>
                  <public type="layout" name="abc_action_bar_title_item" id="0x7f0c0000"/>
                  <public type="menu" name="menu_openchat_info" id="0x7f0d0000"/>
                  <public type="mipmap" name="app_icon" id="0x7f0e0000"/>
                  <public type="raw" name="firebase_common_keep" id="0x7f0f0000"/>
                  <public type="string" name="FreeformWindowOrientation_landscape" id="0x7f100000"/>
                  <public type="style" name="AlertDialog.AppCompat" id="0x7f110000"/>
                  <public type="xml" name="tab_menu" id="0x7f130005"/>
              </resources>

              可以看到,其中包含了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 <init>()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的内容是通过<declare-styleable>生成的,每个<declare-styleable>对应一个attr数组和数个下标值。例如:

              <declare-styleable name="ColorStateListItem">
                  <attr name="android:color"/>
                  <attr format="float" name="alpha"/>
                  <attr name="android:alpha"/>
                  <attr format="float" name="lStar"/>
                  <attr name="android:lStar"/>
              </declare-styleable>

              对应

              .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

              对于下标,我们可以忽略,因为只要数组大小和顺序不变,下标也不需要变。

              那么我们需要修改数组内的值,但是我们可以发现,数组的内容并没有和定义放在一起,那么我们要怎样处理?

              实际上,我们可以忽略数组定义,因为<declare-styleable>并没有新增资源ID,数组内的资源ID都是attr资源ID,所以我们要做的就是:

              • 找到当前文件的旧的资源ID,找到它对应的attr资源名称,我们通过解析扩展包的public.xml就可以获取到旧的资源ID对应的名称。
              • 再通过资源名称找到它在新包中的资源ID值,通过上面保存的新的资源ID很容易可以做到。
              • 替换,这一步需要注意不能全局替换,逐行遍历替换就可以了。

              其他细节

              系统资源

              在资源合并的时候我们有时候会看到一些特别的资源ID,例如0x101011c,这类资源ID为系统资源,实际上我们可以忽略这部分资源ID。也可以在Android源码中找到这些资源ID

              aapt2输出R.java

              除了使用--emit-ids可以获取到所有资源ID之外,也可以选择使用--java来输出R.java,同样可以获取到所有资源ID

              aapt2编译时报系统资源找不到

              需要使用比apk编译版本高的android.jar

              总结

              希望对大家有帮助,欢迎在评论区一起交流~

              以上就是一文带你搞清楚Android游戏发行切包资源ID那点事的详细内容,更多关于Android 游戏发行切包资源ID的资料请关注自由互联其它相关文章!

              上一篇:Flutter替换字符串中的html标签
              下一篇:没有了
              网友评论