在 Android 开发中,我们平时需要使用一些资源时,都需要传入 Resource ID。当然,这个 ID 是编译的时候自动生成的,我们不可能去手动填入这个 ID 值,而是通过 R
来获取这个 Resource ID,比如 R.layout.activity_main
或者 R.mipmap.ic_launcher
等。
但是在开发 SDK 的时候,我们并不推荐使用这种方式来开发。
为什么呢?你可能会写个 Module 打成 AAR 包验证一下并认为没有问题,的确,假如你所写的 SDK 是在你们团队内部使用的话,正常情况下的确不会有问题。
而当你的 SDK 需要提供给其他团队接入,甚至是给其他个人或公司的开发者接入,就不得不考虑兼容性问题。
假如某些开发团队需要使用 JAR 的方式来接入(可前往『「Android Studio」如何导入 AAR 包』一文了解),而你在 SDK 内部通过 R
来获取 Resource ID,很可能就会报类似下面的异常:
E/AndroidRuntime: FATAL EXCEPTION: main
Process: <applicationId>, PID: 1290
java.lang.NoClassDefFoundError: Failed resolution of: L<package>/R$layout;
at ...
Caused by: java.lang.ClassNotFoundException: Didn't find class "<package>.R$layout" on path: DexPathList[[zip file "/data/app/<package>-7LVvXGSzJoLlgcJqzY5uYg==/base.apk"],nativeLibraryDirectories=[/data/app/<package>-7LVvXGSzJoLlgcJqzY5uYg==/lib/arm64, /system/lib64, /system/product/lib64, /hw_product/lib64, /system/product/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at ...
因为使用 JAR 的方式接入时,我们会将资源文件复制到我们项目的资源文件夹中,而不是如 AAR 一样直接引用,因此经过重新编译 AAPT(Android Asset Packaging Tool,Android 资源打包工具,可将资源文件编译成二进制文件)会给对应的资源文件生成一个新的 Resource ID。
我们在代码中通过 R
获取 Resource ID 实际上相当于将原来的 ID 硬编码到 APK 中,即 setContentView(R.layout.activity_main)
编译完成后相当于 setContentView(2131361821)
(详见『Android 反编译入门指南』一文),那么新生成的 ID 和原有的 ID 不相同,自然就抛出找不到 Resource ID 的异常了。
在 SDK 中,我们常会使用动态获取 Resource ID 的方法来避免以上的坑。
Android 为我们提供了 getIdentifier()
方法:
/**
* Return a resource identifier for the given resource name. A fully
* qualified resource name is of the form "package:type/entry". The first
* two components (package and type) are optional if defType and
* defPackage, respectively, are specified here.
*
* <p>Note: use of this function is discouraged. It is much more
* efficient to retrieve resources by identifier than by name.
*
* @param name The name of the desired resource.
* @param defType Optional default resource type to find, if "type/" is not included in the name. Can be null to require an explicit type.
* @param defPackage Optional default package to find, if "package:" is not included in the name. Can be null to require an explicit package.
*
* @return int The associated resource identifier. Returns 0 if no such resource was found. (0 is not a valid resource ID.)
*/
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}
该方法本质上是通过反射调用实现获取资源的 int
型的 ID 数值,所以并不高效,也不鼓励使用。
我们只需传入资源名称、资源类型以及包名就可以获取到对应资源的 ID 了,比如:
public class ModelActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getResourceId(this, "activity_model", "layout"));
}
public static int getResourceId(Context context, String name, String defType) {
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
}
当然,我们可以封装成一个工具类,让使用更加方便:
public class ResourceUtil {
public static int getResourceId(Context context, String name, String defType) {
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
public static int getAnimId(Context context, String name) {
return getResourceId(context, name, "anim");
}
public static int getAnimatorId(Context context, String name) {
return getResourceId(context, name, "animator");
}
public static int getAttrId(Context context, String name) {
return getResourceId(context, name, "attr");
}
public static int getBoolId(Context context, String name) {
return getResourceId(context, name, "bool");
}
public static int getColorId(Context context, String name) {
return getResourceId(context, name, "color");
}
public static int getDimenId(Context context, String name) {
return getResourceId(context, name, "dimen");
}
public static int getDrawableId(Context context, String name) {
return getResourceId(context, name, "drawable");
}
public static int getComponentId(Context context, String name) {
return getResourceId(context, name, "id");
}
public static int getIntegerId(Context context, String name) {
return getResourceId(context, name, "integer");
}
public static int getInterpolatorId(Context context, String name) {
return getResourceId(context, name, "interpolator");
}
public static int getLayoutId(Context context, String name) {
return getResourceId(context, name, "layout");
}
public static int getMipmapId(Context context, String name) {
return getResourceId(context, name, "mipmap");
}
public static int getPluralsId(Context context, String name) {
return getResourceId(context, name, "plurals");
}
public static int getStringId(Context context, String name) {
return getResourceId(context, name, "string");
}
public static int getStyleId(Context context, String name) {
return getResourceId(context, name, "style");
}
public static int getStyleableId(Context context, String name) {
return getResourceId(context, name, "styleable");
}
public static int getXmlId(Context context, String name) {
return getResourceId(context, name, "xml");
}
}
使用以上封装的方法,你可以减少传入的参数,却能够更加清晰的知道自己所获取的资源类型,十分方便。
基于这个工具类,你甚至还可以实现一些有趣的骚操作,比如:
ImageView[] imageViews = new ImageView[NUMBER];
for (int i = 0; i < NUMBER; i++) {
imageViews[i] = findViewById(ResourceUtil.getComponentId(context, "image_" + i));
}
当然这个例子并不那么合适,因为有更优雅的实现方式,只是想通过这个例子来表达,我们可以发挥我们的想象,来创作更多的应用场景。