在 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));
}

当然这个例子并不那么合适,因为有更优雅的实现方式,只是想通过这个例子来表达,我们可以发挥我们的想象,来创作更多的应用场景。