Android开发中,我们常会使用一些依赖注入的框架(比如xutils)来节约我们初始化View以及View的事件的代码量。但是当我们准备在Module中使用这些东西的时候却发现R文件中的Id并不是常量,而依赖注入中的参数必须是常量值,这该如何是好?
想一想造成这个问题的根本原因是什么?id的非常量问题。如果我们能想办法将id更改为常量,问题岂不就是得到了解决吗?
想要修改id为常量,直接更改R文件是不可能了,那就只能想办法复制出一份与R文件相同的类出来,而区别只是所有的field添加final标记符。有了这个解决方案,那么就开干吧。
第一步,我们来寻找一下R文件的生成时机,也就是生成R文件的Task是哪一个。gradle中每个task都有input和output,我们在build文件中寻找R文件的位置发现在:module/build/generated/source/r/debug/packageName/R.java。
为了寻找是哪个Task生成了R文件,我们在build.gradle中加入如下代码:
afterEvaluate {
tasks.all {
it.outputs.files.each { file->
if(file.absolutePath.contains('build/generated/source/r'){
println 'generated->'+it.name
}
}
}
运行后结果如下:
generated->processDebugAndroidTestResources
generated->processDebugResources
generated->processReleaseResources
从结果可以看到gradle根据不同的场景(Debug、Release、AndroidTest)有不同的Task与之对应,至此已经找到生成R文件的Task。
第二步,上一步我们找到了Task,我们还需要解决怎么生成R文件的副本文件。
第一种方式:直接复制R文件,添加final关键字,这样的话新的文件包含了所有类型的id值。那有没有办法简单的只取R.id的值呢?
于是我们再去build结果中寻找,经过寻找,我们意外发现在build/intermediates/symbols/debug 以及build/intermediates/bundles/debug中找到了一个R.txt的文件,打开后发现是这样的
int anim abc_fade_in 0x7f050000
int anim abc_fade_out 0x7f050001
int anim abc_grow_fade_in_from_bottom 0x7f050002
int anim abc_popup_enter 0x7f050003
int anim abc_popup_exit 0x7f050004
int anim abc_shrink_fade_out_from_bottom 0x7f050005
int anim abc_slide_in_bottom 0x7f050006
int anim abc_slide_in_top 0x7f050007
int anim abc_slide_out_bottom 0x7f050008
从文件内容来看,这个文件应该是用来做所有module的R文件的merge的时候的中间文件,这却刚好方便了我们。
文件中每一行是4段内容,每段内容由空格分开分别是:
[数据类型] [值类型(子类名称)] [字段名称] [字段值]
int anim abc_slide_out_bottom 0x7f050008
public static final class anim {
public static final int abc_slide_out_bottom = 0x7f050008;
}
经过这样分析,我们可以将这个文件作为我们自己的Task的input,使用同样的方式生成另一个R文件的副本K.java。不过R.txt中还有一些是int[]类型的,这样的内容我们暂时可以跳过。于是我们有了另一种方式。
第二种方式:解析R.txt文件,摘取其中的ID类型的值,同样的方法也可以筛选其他类型的值。
第三步,自定义Task生成K.java文件。
我们先看第二种方式的实现方式。
1、在/buildSrc/src/main/groovy/packageName/中添加GenerateK.groovy文件。内容如下:
import org.gradle.api.Project
import org.gradle.api.Task
public static autoGenerateR(Project projcet, Task task) {
File inputR = task.inputs.files.files.toArray()[0]
File outDir = task.outputs.files.files.toArray()[0]
def manifestFile = projcet.android.sourceSets.main.manifest.srcFile
def packageName = new XmlParser().parse(manifestFile).attribute('package')
File file = new File(inputR, 'R.txt')
StringBuffer stringBuffer = new StringBuffer()
HashMap<String, List> fieldHash = new HashMap<>()
file.readLines().each {
String[] fields = it.split(' ')
if (fields.length == 4) {
List tmpList = fieldHash.get(fields[1])
if (tmpList == null) {
tmpList = new ArrayList();
}
if (fields[1].equals('id')) {
tmpList.add('public static final ' + fields[0] + ' ' + fields[2] + ' = ' + fields[3] + ' ;')
fieldHash.put(fields[1], tmpList)
}
}
}
stringBuffer.append('package ' + packageName + ';\n')
stringBuffer.append('public final class K { \n')
fieldHash.each { k, v ->
stringBuffer.append(' public static final class ' + k + ' { \n')
v.each {
stringBuffer.append(' ' + it + '\n')
}
stringBuffer.append(' }\n')
}
stringBuffer.append('}\n')
File destFile = new File(outDir, '/' + packageName.toString().replace('.', '/') + '/K.java')
if (!destFile.parentFile.exists()) {
destFile.parentFile.mkdirs()
}
destFile.write(stringBuffer.toString(), 'utf-8')
}
2、在build.gradle中添加如下代码:
afterEvaluate {
getTasks().all { tsk ->
if (tsk.name.endsWith("Resources")
&& tsk.name.startsWith("process")
&& !tsk.name.contains('AndroidTest')) {
def buildType = tsk.name.replace("process", "").replace("Resources", "")
def taskK = task("build" + buildType + "K", dependsOn: tsk) {}
tsk.outputs.files.each {
if (it.absolutePath.contains('generated/source/r')) {
taskK.outputs.file(it.absolutePath)
}
if (it.absolutePath.contains('intermediates/symbols')
|| it.absolutePath.contains('intermediates/bundles/')) {
taskK.inputs.file(it.absolutePath)
}
}
taskK.doLast {
GenerateK.autoGenerateR(project, taskK)
}
tsk.doLast {
GenerateK.autoGenerateR(project, taskK)
}
}
}
}
3、执行buildDebugK或者buildReleaseK
现在,我们什么都准备好了,直接执行assembleDebug或者assembleRelease,或者执行buildDebugK或者buildReleaseK就都能生成K.java文件啦。文件位置在:module/build/generated/source/r/debug/packageName/K.java。
现在我们再用第一种方式实现:
1、在上一种实现方式的GenerateK.groovy文件中加入如下代码:
public static autoGenerateK(Project projcet, Task task) {
File inputR = task.inputs.files.files.toArray()[0]
File outDir = task.outputs.files.files.toArray()[0]
def manifestFile = projcet.android.sourceSets.main.manifest.srcFile
def packageName = new XmlParser().parse(manifestFile).attribute('package')
String packageDir = packageName.toString().replace('.', '/')
File rFile = new File(outDir, packageDir + '/R.java')
StringBuffer rStringBuffer = new StringBuffer();
rFile.readLines().each {
rStringBuffer.append(it + '\n')
}
String kFileContent = rStringBuffer.toString().replace('public static int', 'public static final int')
kFileContent = kFileContent.replace('public final class R', 'public final class K')
File destFile = new File(outDir, '/' + packageName.toString().replace('.', '/') + '/K.java')
if (!destFile.parentFile.exists()) {
destFile.parentFile.mkdirs()
}
destFile.write(kFileContent, 'utf-8')
}
2、修改上一种实现方式的第二步的GenerateK.autoGenerateR(project, taskK)改成GenerateK.autoGenerateK(project, taskK),然后同样的再执行第三步。
打开看看吧,然后把原来注解需要R.id的地方都替换成K.id试试,是不是满足了我们的需求呢?
其实到这里我们已经完工了,但是却不完美,因为每次id增加/删除/修改时都无法实时的在代码提示中收到反馈,需要执行一次buildXXXK这个Task(虽然很快),这个问题…有待研究,或许做一个Android Studio的插件可以达到效果~~
不过,R文件也只支持增加Id而不支持删除Id的实时反馈。