背景
一般而言是不需要手动生成R.java文件的,对app开发而言,无疑是画蛇添足;对sdk开发而言,因为Android提供了aar的依赖方式,可以将资源文件一起打包入aar,最后集成方一起编辑生成R.java即可。
然而,快要2019年了,仍然有一些强势的集成方/游戏开发商仍然在使用Eclipse开发,不支持aar的依赖方式,要求SDK是jar包的形式。对于sdk包含UI的开发方而言是十分痛苦的,只能提供jar包+res的形式提供sdk。
这种提供jar包+res的业务场景下就需要SDK开发者改变资源的获取方式,不能再通过原生R.String.xxx的方式获取资源,因为只有最后一次编译的时候才能确定资源的ID,之前的任何一次打包产生的ID值都是没有意义的。
传统的解决方法是:
每一处使用res的地方,全部要从R.String.xxx的方式修改为mContext.getResources().getIdentifier(resName, resType, mPackageName)的方式获取资源。
然而这个方案存在2个问题:
- 没有代码补全码提示、没有强类型提示,当写错了资源名或类型,只有在运行期才会报错,不像
R.String.xxx这种形式,有很好的提示效果,在编码和编译阶段就能排除这种低级错误。 - 性能较差
经过分析,需要做两件事情:
- 自动生成一个类似R.java一样的文件AutoR.java,包括所有的资源类型的引用,这个java文件最终一起打包进SDK的jar包,所有资源引用的方式为
AutoR.type.name - 通过资源名和资源类型可以获取到宿主APP最终打包后的资源ID值
通过资源名和资源类型获取ID
通过资源名和资源类型可以获取资源ID,方式有两种,代码如下:
第一种反射宿主app的R文件方式,注意这里的mPackageName是宿主的package:
Class<?> cls = Class.forName(mPackageName + ".R$" + resType);
return cls.getField(resName).getInt(cls);
第二种:调用系统api获取:
mContext.getResources().getIdentifier(resName, resType, mPackageName);
资源的获取方面: 除了Android系统自带的资源第一种反射的方式无法获取外,两者几乎是等价的,不存在某一种方式能获取到资源,另一种却获取不到的情况。
性能方面:
测试循环一万次,第一种反射的方式在2014年macbookpro上耗时157ms;
第二种方式耗时1900ms,而R.string.xxx的方式,循环1万次的耗时是0ms……
因此显然:
应该优先使用反射方式获取资源文件。
代码如下:
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.Log;
/* AUTO-GENERATED FILE. DO NOT MODIFY.
*
* This class was automatically generated by the
* AutoR tool .
* It should not be modified by hand.
*/
public final class AutoR {
private static String mPackageName;
@SuppressLint("StaticFieldLeak")
private static Context mContext;
private static volatile boolean RExist;
public static void init(Context context){
if (context != null) {
mContext = context;
mPackageName = context.getPackageName();
try {
Class.forName(mPackageName + ".R");
RExist = true;
}
catch (ClassNotFoundException e) {
//some game situation
Log.i("autoR", "No R class");
RExist =false;
}
}else {
Log.e("autoR","don't init AutoR with null");
}
}
private static int getResId(String resName, String resType) {
if (mContext != null) {
//android system resource
if (!resName.isEmpty() && resName.startsWith("android_")) {
return mContext.getResources().getIdentifier(resName.replace("android_", ""), resType, "android");
}
// R exist
if (RExist) {
try {
Class<?> cls = Class.forName(mPackageName + ".R$" + resType);
return cls.getField(resName).getInt(cls);
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.e("autoR", "IllegalAccessException:" + e.getMessage());
} catch (ClassNotFoundException e) {
e.printStackTrace();
Log.e("autoR", "ClassNotFoundException:" + e.getMessage());
} catch (NoSuchFieldException e) {
e.printStackTrace();
Log.e("autoR", "NoSuchFieldException:" + e.getMessage());
}
} else {
return mContext.getResources().getIdentifier(resName, resType, mPackageName);
}
}else {
Log.e("autoR","you should init AutoR first");
}
return 0;
}
}
在你sdk的初始方法中调用AutoR.inti(context.getApplicationContext)即可。为了避免内存泄露,这里的context应该是ApplcationContext。
第一个问题就此解决。
自动生成AutoR.java文件
其实很简单,打开任何一份系统自动生成的R.java文件,类似如下:
。。。。
public static final class string {
public static int status_string = 0x7f150202;
。。。
}
。。。
这是一个String类型的资源,系统编译的时候会自动确定他的常量ID值,我们需要做的就是将这个ID值,替换为上一节中的AutoR.getResId("status_string","string")
因此,我们的策略:
- 系统prebuild task之前插入一个task,手动先调用Android sdk的aapt命令生产R.java,然后根据R.java生成AutoR.java
- 同时将sdk源码中的import xx.R全部替换为import xx.AutoR
- 将所有调用资源的地方修改为
AutoR.string.xxxx
//编译之前运行AutoR
task autoR(type: Exec) {
println 'before preBuild'
commandLine 'python3','../autoR/autoR.py'
}
preBuild.dependsOn autoR
最后生成的AutoR.java类似如下:
...
public static final class anim {
public static int activity_close_enter = getResId("activity_close_enter", "anim");
public static int activity_close_exit = getResId("activity_close_exit", "anim");
public static int activity_in_right = getResId("activity_in_right", "anim");
......
public static final class attr {
public static int isLoop = getResId("isLoop", "attr");
public static int barContent = getResId("barContent", "attr");
......
具体不多说,可以参考源码。
注:目前只兼容了Mac和Linux,未支持windows,有兴趣的可以修改,应该是目录之类的问题。
源码地址AutoR