前言

资源id的固定在热修复和插件化中极其重要。在热修复中,构建patch时,需要保持patch包的资源id和基线包的资源id一致;在插件化中,如果插件需要引用宿主的资源,则需要将宿主的资源id进行固定,因此,资源id的固定在这两种场景下是尤为重要的。而在Android Gradle Plugin 3.0.0中,默认开启了aapt2,原先aapt的资源固定方式public.xml也将失效,必须寻找一种新的资源固定的方式,而不是简单的禁用掉aapt2,因此本文来探讨一下开启aapt2的情况下如何进行资源id的固定。

aapt的资源固定方式

在探索aapt2资源固定方式前,先来温习一下aapt原先的资源固定方式。

  • 编译基线包时添加aapt参数-P导出public.xml文件
  • 编译插件或者patch包时,将public.xml文件拷贝至资源merge完成的目录,并根据values.xml中的定义和public.xml中的定义,选择性的生成ids.xml文件

    对应的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
apply plugin: PublicPlugin
class PublicPlugin implements Plugin<Project> {
void apply(Project project) {
project.afterEvaluate {
if (project.plugins.hasPlugin("com.android.application")) {
def android = project.extensions.getByName("android")
android.applicationVariants.all {def variant ->
File publicXmlFile = project.rootProject.file('public.xml')
//public文件存在则应用,不存在则生成
if (publicXmlFile.exists()) {
//aapt的应用需要将文件拷贝到对应的目录
//aapt public.xml文件的应用并不是只是拷贝public.xml文件那么简单,还要根据生成的public.xml生成ids.xml文件,并将ids.xml中与values.xml中重复定义的id去除
String mergeResourcesTaskName = variant.variantData.getScope().getMergeResourcesTask().name
def mergeResourcesTask = project.tasks.getByName(mergeResourcesTaskName)
//资源merge的task存在则在其merge完资源后拷贝public.xml并生成ids.xml
if (mergeResourcesTask) {
mergeResourcesTask.doLast {
//拷贝public.xml文件
File toDir = new File(mergeResourcesTask.outputDir, "values")
project.copy {
project.logger.error "${variant.name}:copy from ${publicXmlFile.getAbsolutePath()} to ${toDir}/public.xml"
from(publicXmlFile.getParentFile()) {
include "public.xml"
rename "public.xml", "public.xml"
}
into(toDir)
}
//生成ids.xml文件
File valuesFile = new File(toDir, "values.xml")
File idsFile = new File(toDir, "ids.xml")
if (valuesFile.exists() && publicXmlFile.exists()) {
//记录在values.xml中存在的id定义
def valuesNodes = new XmlParser().parse(valuesFile)
Set<String> existIdItems = new HashSet<String>()
valuesNodes.each {
if ("id".equalsIgnoreCase("${it.@type}")) {
existIdItems.add("${it.@name}")
}
}
GFileUtils.deleteQuietly(idsFile)
GFileUtils.touch(idsFile)
idsFile.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
idsFile.append("\n")
idsFile.append("<resources>")
idsFile.append("\n")
def publicXMLNodes = new XmlParser().parse(publicXmlFile)
Pattern drawableGeneratePattern = Pattern.compile('^(.*?_)([0-9]{0,})$')
publicXMLNodes.each {
//获取public.xml中定义的id类型item
if ("id".equalsIgnoreCase("${it.@type}")) {
//如果在values.xml中没有定义,则添加到ids.xml中
//如果已经在values.xml中定义,则忽略它
if (!existIdItems.contains("${it.@name}")) {
idsFile.append("\t<item type=\"id\" name=\"${it.@name}\" />\n")
} else {
project.logger.error "already exist id item ${it.@name}, ignore it"
}
} else if ("drawable".equalsIgnoreCase("${it.@type}")) {
//以'_数字'结尾的drawable资源,此类资源是aapt编译时生成的nested资源,如avd_hide_password_1, avd_hide_password_2
//但是可能会有其他资源掺杂,如abc_btn_check_to_on_mtrl_000, abc_btn_check_to_on_mtrl_015
//为了将此类资源过滤掉,将正则匹配到的数字转成int,对比原始数字部分匹配字符串,如果一致,则是aapt生成
//重要:为了避免此类nested资源生成顺序发生改变,应该禁止修改此类资源
Matcher matcher = drawableGeneratePattern.matcher(it.@name)
if (matcher.matches() && matcher.groupCount() == 2) {
String number = matcher.group(2)
if (number.equalsIgnoreCase(Integer.parseInt(number).toString())) {
project.logger.info "[${PREFIX}] declared drawable resource ${it.@name} which is generated by aapt. like use '<aapt:attr name=\"android:drawable\">'"
idsFile.append("\t<item type=\"drawable\" name=\"${it.@name}\" />\n")
}
}
}
}
idsFile.append("</resources>")
}
}
}
} else {
//不存在则生成
project.logger.error "${publicXmlFile} not exists, generate it"
//aapt 添加-P参数生成
aaptOptions.additionalParameters("-P", "${publicXmlFile}")
}
}
}
}
}
}

很简单,生成public.xml时使用aapt的参数-P,指定生成的文件路径即可;应用public.xml则将其拷贝到values目录下,唯一需要注意的是这个导出的public.xml文件会存在资源id未定义的情况,因此需要生成ids.xml文件,对未定义的id类型资源进行定义。而这个生成的ids.xml文件,可能与values/values.xml文件中的id存在重复定义的现象,因此生成的时候,则需要判断对应的id名在values.xml文件中是否存在,如果存在则直接忽略,因为它已经定义了;如果不存在,则添加到ids.xml中进行定义。

这种方式不支持删除现有的资源,如果删除了现有的资源,public.xml中的定义也得删除,否则会报资源未定义的错误。

aapt2的资源固定方式

那么在aapt2中上面这种方式还生效吗,答案是否定的,至于为什么,可以参考之前的一篇文章aapt2 资源 compile 过程,因为所有merge的资源都已经经过了预编译,产生了flat文件,这时候将public.xml文件拷贝至该目录就会产生编译错误。那么如何解决了。通过查看Android Gradle Plugin 3.0.0的代码发现了一些猫腻,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public static ImmutableList<String> makeLink(
@NonNull AaptPackageConfig config, @NonNull File intermediateDir) throws AaptException {
ImmutableList.Builder<String> builder = ImmutableList.builder();
if (config.isVerbose()) {
builder.add("-v");
}
File stableResourceIdsFile = new File(intermediateDir, "stable-resource-ids.txt");
// TODO: For now, we ignore this file, but as soon as aapt2 supports it, we'll use it.
//此处省略n行代码
}

大概可以猜测可以通过指定稳定的资源id映射文件达到固定资源id的作用,但是代码中这个文件并没有共同参与资源编译的过程,因此这部分代码暂时无效,接下来去aapt2命令中寻找一下。通过aapt2 link –help可以发现有两个参数。如下

1
2
--stable-ids arg File containing a list of name to ID mapping.
--emit-ids arg Emit a file at the given path with a list of name to ID mappings, suitable for use with --stable-ids.

大概意思就是说可以通过–emit-ids参数指定一个文件,该文件会输出资源名字到资源id的一个映射,这个文件可以被–stable-ids参数使用。

通过简单的测试,发现这两个参数可以完全满足我们的需求。而且这种方式支持删除现有的资源。编写代码验证之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apply plugin: PublicPlugin
class PublicPlugin implements Plugin<Project> {
void apply(Project project) {
project.afterEvaluate {
if (project.plugins.hasPlugin("com.android.application")) {
def android = project.extensions.getByName("android")
android.applicationVariants.all {def variant ->
def processResourcesTask = project.tasks.getByName("process${variant.name.capitalize()}Resources")
if (processResourcesTask) {
def aaptOptions = processResourcesTask.aaptOptions
File publicTxtFile = project.rootProject.file('public.txt')
//public文件存在,则应用,不存在则生成
if (publicTxtFile.exists()) {
project.logger.error "${publicTxtFile} exists, apply it."
//aapt2添加--stable-ids参数应用
aaptOptions.additionalParameters("--stable-ids", "${publicTxtFile}")
} else {
project.logger.error "${publicTxtFile} not exists, generate it."
//aapt2添加--emit-ids参数生成
aaptOptions.additionalParameters("--emit-ids", "${publicTxtFile}")
}
}
}
}
}
}
}

代码很简单,就是当public.txt文件不存在时,添加–emit-ids参数进行生产,如果存在时,则添加–stable-ids进行应用。

简单验证一下,定义3个颜色资源

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorAccent">#FF4081</color>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
</resources>

编译,可以看到项目根目录下产生了public.txt文件,其中这三个资源对应的内容为

1
2
3
io.github.lizhangqu.aapt2:color/colorAccent = 0x7f040026
io.github.lizhangqu.aapt2:color/colorPrimary = 0x7f040027
io.github.lizhangqu.aapt2:color/colorPrimaryDark = 0x7f040028

在这三个资源中插入一个资源,打乱资源顺序,如下

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorAccent">#FF4081</color>
<color name="colorAccentBBBBB">#FF4081</color>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
</resources>

将刚才生成的public.txt文件备份至其他目录,删除根目录下的public.txt文件,保证重新生成该文件,重新编译资源,此时生成的public.txt文件中的这四个资源对应的内容为

1
2
3
4
io.github.lizhangqu.aapt2:color/colorAccent = 0x7f040026
io.github.lizhangqu.aapt2:color/colorAccentBBBBB = 0x7f040027
io.github.lizhangqu.aapt2:color/colorPrimary = 0x7f040028
io.github.lizhangqu.aapt2:color/colorPrimaryDark = 0x7f040029

app/build/intermediates/res/symbol-table-with-package/debug/package-aware-r.txt文件中对应的资源id为

1
2
3
4
int color colorAccent 0x7f040026
int color colorAccentBBBBB 0x7f040027
int color colorPrimary 0x7f040028
int color colorPrimaryDark 0x7f040029

两个文件中的内容完全可以对的上,只要看其中一个就可以了;可以看到由于colorAccentBBBBB的插入,colorPrimary和colorPrimaryDark都向后顺延了一位,也就是说,在没有资源固定的情况下,如果增删改等操作发生,是有可能导致现有资源id发生变化的。

因此我们开始验证–stable-ids参数的有效性,将根目录下的public.txt文件删除,将之前备份好的public.txt文件拷贝到根目录,重新编译资源。这时候编译产生的资源id映射为

1
2
3
4
int color colorAccent 0x7f040026
int color colorAccentBBBBB 0x7f040057
int color colorPrimary 0x7f040027
int color colorPrimaryDark 0x7f040028

可以看到colorAccent,colorPrimary和colorPrimaryDark的资源id并没有因为colorAccentBBBBB的插入而发生变化,而是保持了原有的资源id,而新增的资源colorAccentBBBBB则是重新分配了一个新的资源id。

至此,基本可以确定该方案是可行的(但不保证有没有坑)

适配aapt和aapt2

因此,需要进行aapt的版本判断,适配不同的情况,这里我已经把代码写好了,基本上就是对上面两段代码的组合,其中aapt版本的获取需要捕获一下异常,该函数在低版本中不存在,具体实现看代码,这里不再过多解释了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
apply plugin: PublicPlugin
class PublicPlugin implements Plugin<Project> {
void apply(Project project) {
project.afterEvaluate {
if (project.plugins.hasPlugin("com.android.application")) {
def android = project.extensions.getByName("android")
android.applicationVariants.all {def variant ->
boolean aapt2Enable = false
def processResourcesTask = project.tasks.getByName("process${variant.name.capitalize()}Resources")
if (processResourcesTask) {
try {
//判断aapt2是否开启,低版本不存在这个方法,因此需要捕获异常
aapt2Enable = processResourcesTask.isAapt2Enabled()
} catch (Exception e) {
project.logger.error "${e.getMessage()}"
}
def aaptOptions = processResourcesTask.aaptOptions
//aapt2开启走此流程
if (aapt2Enable) {
project.logger.error "aapt2 is enabled"
File publicTxtFile = project.rootProject.file('public.txt')
//public文件存在,则应用,不存在则生成
if (publicTxtFile.exists()) {
project.logger.error "${publicTxtFile} exists, apply it."
//aapt2添加--stable-ids参数应用
aaptOptions.additionalParameters("--stable-ids", "${publicTxtFile}")
} else {
project.logger.error "${publicTxtFile} not exists, generate it."
//aapt2添加--emit-ids参数生成
aaptOptions.additionalParameters("--emit-ids", "${publicTxtFile}")
}
} else {
//aapt2禁用走此流程
project.logger.error "aapt2 is disabled"
File publicXmlFile = project.rootProject.file('public.xml')
//public文件存在则应用,不存在则生成
if (publicXmlFile.exists()) {
//aapt的应用需要将文件拷贝到对应的目录
//aapt public.xml文件的应用并不是只是拷贝public.xml文件那么简单,还要根据生成的public.xml生成ids.xml文件,并将ids.xml中与values.xml中重复定义的id去除
String mergeResourcesTaskName = variant.variantData.getScope().getMergeResourcesTask().name
def mergeResourcesTask = project.tasks.getByName(mergeResourcesTaskName)
if (mergeResourcesTask) {
mergeResourcesTask.doLast {
//拷贝public.xml文件
File toDir = new File(mergeResourcesTask.outputDir, "values")
project.copy {
project.logger.error "${variant.name}:copy from ${publicXmlFile.getAbsolutePath()} to ${toDir}/public.xml"
from(publicXmlFile.getParentFile()) {
include "public.xml"
rename "public.xml", "public.xml"
}
into(toDir)
}
//生成ids.xml文件
File valuesFile = new File(toDir, "values.xml")
File idsFile = new File(toDir, "ids.xml")
if (valuesFile.exists() && publicXmlFile.exists()) {
//记录在values.xml中存在的id定义
def valuesNodes = new XmlParser().parse(valuesFile)
Set<String> existIdItems = new HashSet<String>()
valuesNodes.each {
if ("id".equalsIgnoreCase("${it.@type}")) {
existIdItems.add("${it.@name}")
}
}
GFileUtils.deleteQuietly(idsFile)
GFileUtils.touch(idsFile)
idsFile.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>")
idsFile.append("\n")
idsFile.append("<resources>")
idsFile.append("\n")
def publicXMLNodes = new XmlParser().parse(publicXmlFile)
Pattern drawableGeneratePattern = Pattern.compile('^(.*?_)([0-9]{0,})$')
publicXMLNodes.each {
//获取public.xml中定义的id类型item
if ("id".equalsIgnoreCase("${it.@type}")) {
//如果在values.xml中没有定义,则添加到ids.xml中
//如果已经在values.xml中定义,则忽略它
if (!existIdItems.contains("${it.@name}")) {
idsFile.append("\t<item type=\"id\" name=\"${it.@name}\" />\n")
} else {
project.logger.error "already exist id item ${it.@name}, ignore it"
}
} else if ("drawable".equalsIgnoreCase("${it.@type}")) {
//以'_数字'结尾的drawable资源,此类资源是aapt编译时生成的nested资源,如avd_hide_password_1, avd_hide_password_2
//但是可能会有其他资源掺杂,如abc_btn_check_to_on_mtrl_000, abc_btn_check_to_on_mtrl_015
//为了将此类资源过滤掉,将正则匹配到的数字转成int,对比原始数字部分匹配字符串,如果一致,则是aapt生成
//重要:为了避免此类nested资源生成顺序发生改变,应该禁止修改此类资源
Matcher matcher = drawableGeneratePattern.matcher(it.@name)
if (matcher.matches() && matcher.groupCount() == 2) {
String number = matcher.group(2)
if (number.equalsIgnoreCase(Integer.parseInt(number).toString())) {
project.logger.info "[${PREFIX}] declared drawable resource ${it.@name} which is generated by aapt. like use '<aapt:attr name=\"android:drawable\">'"
idsFile.append("\t<item type=\"drawable\" name=\"${it.@name}\" />\n")
}
}
}
}
idsFile.append("</resources>")
}
}
}
} else {
//不存在则生成
project.logger.error "${publicXmlFile} not exists, generate it"
//aapt 添加-P参数生成
aaptOptions.additionalParameters("-P", "${publicXmlFile}")
}
}
}
}
}
}
}
}

另一种解决方式

如果项目确定使用的是aapt2,并且不想通过编写插件解决,这里提供一种更加简单的方式,就是直接利用aaptOptions参数进行指定,但是这种方式不好做aapt和aapt2之间的无缝适配,只适合aapt2,参考代码如下

1
2
3
4
5
6
7
8
9
10
android {
aaptOptions {
File publicTxtFile = project.rootProject.file('public.txt')
if (publicTxtFile.exists()) {
additionalParameters "--stable-ids", "${project.rootProject.file('public.txt').absolutePath}"
} else {
additionalParameters "--emit-ids", "${project.rootProject.file('public.txt').absolutePath}"
}
}
}

public.xml到public.txt的转换

如果之前是用aapt备份下来的public.xml,如果现在使用了aapt2,则需要将文件进行转换,转换方式也很简单,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
task convertPublicXmlToPublicTxt() {
doLast {
//源public.xml
File publicXmlFile = project.rootProject.file('backup/public.xml')
//目标public.txt
File publicTxtFile = project.rootProject.file('backup/generate_public.txt')
//包名
String applicationId = "io.github.lizhangqu.aapt2"
GFileUtils.deleteQuietly(publicTxtFile)
GFileUtils.touch(publicTxtFile)
def nodes = new XmlParser().parse(publicXmlFile)
Pattern drawableGeneratePattern = Pattern.compile('^(.*?_)([0-9]{0,})$')
nodes.each {
project.logger.error "${it}"
if ("drawable".equalsIgnoreCase("${it.@type}")) {
//以'_数字'结尾的drawable资源,此类资源是aapt编译时生成的nested资源,如avd_hide_password_1, avd_hide_password_2
//但是可能会有其他资源掺杂,如abc_btn_check_to_on_mtrl_000, abc_btn_check_to_on_mtrl_015
//为了将此类资源过滤掉,将正则匹配到的数字转成int,对比原始数字部分匹配字符串,如果一致,则是aapt生成
//重要:为了避免此类nested资源生成顺序发生改变,应该禁止修改此类资源
//aapt生成的是以下表1开始,aapt2是以下标0开始,因此转换的过程需要-1
Matcher matcher = drawableGeneratePattern.matcher(it.@name)
if (matcher.matches() && matcher.groupCount() == 2) {
String number = matcher.group(2)
if (number.equalsIgnoreCase(Integer.parseInt(number).toString())) {
String prefixName = matcher.group(1)
publicTxtFile.append("${applicationId}:${it.@type}/\$${prefixName}_${Integer.parseInt(number) - 1} = ${it.@id}\n")
return
}
}
}
publicTxtFile.append("${applicationId}:${it.@type}/${it.@name} = ${it.@id}\n")
}
}
}

执行 gradle convertPublicXmlToPublicTxt即可完成转换。

值得注意的是,这种转换方式,由于原先aapt导出的public.xml中没有styleable的定义,所以转换后的public.txt中也没有styleable,即转换后的数据是aapt2导出的数据的子集,而aapt2生成的public.txt是具有styleable类型的id的,但是实际应用过程中并没有发现什么大的问题,因此几乎可以忽略不计。如果你发现有问题,可以及时联系我。

意外的收获aapt2资源分区

在寻找解决aapt2资源id固定的过程中,意外发现aapt2自带了资源PP段分区功能。就是通过–package-id参数,指定PP段分区,但是值得注意的是这个值必须大于0x7f,经过测试,大于0x7f的PP段分区在Android7.0以下是无法识别的,可以安装但是启动会崩溃。因此通过这种方式分区生成的apk文件,只能安装在Android7.0以上的系统,比较鸡肋。示例代码如下

1
2
3
4
5
android {
aaptOptions {
additionalParameters "--package-id", "0x80"
}
}

此时生成的apk的资源PP段都是0x80,可以通过app/build/intermediates/res/symbol-table-with-package/debug/package-aware-r.txt文件进行验证。

总结

当上帝为你关上一扇门的时候,还会用门夹你的脑袋(开个玩笑),虽然public.xml在aapt2中无法用了,但是google在aapt2中提供给我们的–stable-ids和–emit-ids两个参数不见得那么不好用,甚至比aapt的public.xml还要好用,只需要生成和应用就好了,不需要进行中间的处理过程。简直完美!但是该方案还没有用足够多的case进行验证,不代表没有坑,出了坑,不负责!

http://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/

aapt2 适配之资源 id 固定相关推荐

  1. android 指定资源id,Android平台下如何固定资源ID

    文章转自半导体人社群芯方式 [QUESTION] 如何固定资源ID [ANSWER] 一般情况,同一个资源,因为程序的不停修改,不同版本编译后,这个资源产生的ID可能会变化.对于一个独立的apk来说, ...

  2. android 获取应用的资源id和uri

    2019独角兽企业重金招聘Python工程师标准>>> 在某些应用中,为了实现应用apk资源放入重复利用,或者使用反射得到本应用的资源,需要使用反射反射方式获得,但Resources ...

  3. 动态的根据一个资源名获得到对应的资源id

    在做项目的过程中,遇到了一个需求,在Android代码里可以通过图片的ID动态加载图片.要加载Android文件夹里面的图片,我们就要知道它的ID,要动态获取,那么就得动态获取图片的ID. 那么应该怎 ...

  4. 【Android 安装包优化】资源混淆 ( 资源混淆效果 | APK 构建流程简介 | 资源 ID 组成 )

    文章目录 一.资源混淆效果 二.APK 构建流程简介 三.资源 ID 组成 四.参考资料 一.资源混淆效果 资源混淆 , 将资源名称与目录进行混淆 , 提高了反编译的难度 , 同时也减小了 APK 文 ...

  5. 【Android 安装包优化】移除无用资源 ( 自动移除无用资源 | 直接引用资源 | 动态获取资源 id | Lint 检查资源 )

    文章目录 一.自动移除无用资源 ( 不推荐使用 ) 二.直接引用资源与动态获取资源 1.直接引用资源 2.动态获取资源 id 三.Lint 检查资源 四.参考资料 一.自动移除无用资源 ( 不推荐使用 ...

  6. Android开发之SDK开发获取资源id报错的问题

    我们在开发SDK的时候有时候需要兼容Eclipse版本和AS版本,AS版本就不说了没多大问题,主要是Eclipse版本会有问题.当开发之使用Eclipse开发的时候接入jar包(AS打包成aar,解压 ...

  7. Android之使用getIdentifier()获取资源Id

    使用getIdentifier()方法可以方便的获各应用包下的指定资源ID. 主要有两种方法: (1)方式一 Resources resources = context.getResources(); ...

  8. android 指定资源id,Android 通过名称获取资源ID

    当我们获取网络数据的时候,解析之后往往都是一个字符串,而不是资源id,所有我们没有办法直接使用,只能通过名称来获取到资源id, package com.example.administrator.de ...

  9. android工程换背景图片,android换肤功能 如何动态获取控件中背景图片的资源id?

    这个是在在做一个换肤功能时遇到的问题. 对于换肤,网上都有示例,可以从别的皮肤安装包中读取所要的资源,前提是你必须先持有这个资源的引用名称,像R.drawable.background(喂,这不是废话 ...

最新文章

  1. spring boot configuration annotation processor not found in classpath问题解决
  2. 第一个Django页面
  3. 好看的php提示弹窗,漂亮的jquery提示效果(仿腾讯弹出层)
  4. 设置窗体的可见性无效
  5. python数据分析如何学_用Python做数据分析,Numpy,Pandas,matp
  6. Selenium中的xpath定位
  7. [转]余弦cos计算相似度
  8. (转)利用Eclipse创建maven项目并生成META-INF和WEB-INF目录
  9. Java自学!java题库网站
  10. 各个国家的货币符号和英文缩写全解析
  11. opencv: 颜色通道 探究(图示+源码)
  12. BRD、MRD、PRD
  13. 体系切换,华为IPD的研发管理之道(上)
  14. 强化学习常用算法总结
  15. 移动互联时代的地方网站转型
  16. Atari 2600 新书:主机游戏的一次黎明冒险
  17. yarn错误The engine “node“ is incompatible with this module
  18. JS 判断客户端是iOS还是Android
  19. Invalid bound statement (not found): com.example.demo.mapper.getUser.
  20. 数学建模 微分方程实战:利用SEIR模型对新冠病毒预测

热门文章

  1. Centos 6.5下的OPENJDK卸载和SUN的JDK安装、环境变量配置
  2. Android开源项目发现---ProgressBar 篇(持续更新)
  3. 3D 服务器端以向量计算为主的角色位置的算法
  4. Python PycURL 网络编程
  5. Python学习笔记:Day13 提升开发效率
  6. CPU指令集是什么东西
  7. 深入解析C++编程中的静态成员函数
  8. this指针用法详解
  9. 牛客网-斐波那契数列
  10. SVM 透彻理解与分析