Android开发中R资源引用注意事项

1,452 阅读10分钟

我们都知道,在Android打包时进行的aapt是对Android的资源打包,从而生成一个R.java文件,其中包括非assets下的所有资源的id值,同时还生成resource.arsc文件,也就是资源索引表。

而我们程序在运行中,就是从R.java获取具体的id值,再到resource.arsc检索获取相对应的资源。

这文章不是讲Android资源打包的过程,这在老罗的《Android应用程序资源的编译和打包过程分析》(blog.csdn.net/luoshengyan… 一文讲的非常详细(反正我看得晕头转向),而是讲我们平时开发中对Android资源引用的几点注意事项。

由于个人的电脑渣渣和对Eclipse的项目结构更了解,所以我本篇文章的demo是在Eclipse上编写的。但Eclipse上和Android Studio上资源引用是一样的,只是R.java的目录变了,后面也会涉及到Android Studio上的一些问题。

下面讲述对R资源引用的一些个人结论,也是我曾经踩过的坑,对于SDK开发的小伙伴或经常接入第三方SDK的小伙伴可能会比较了解,希望对大家有所帮助和借鉴。

先说下demo的结构:demo有两个项目,一个Sample和一个SampleLib,Sample直接引用SampleLib或导入SampleLib的jar包,从而使用SampleLib里所定义好的接口。由于这个demo只是为了测试R资源引用的注意事项,所以demo非常简单。

在SampleLib只有一个类,类中只包含两个接口,分别是通过直接引用R和代码方式返回一张在drawable里的图片“test”的id:

package com.leo.sample.lib;
![Uploading drawable下的test图片_504217.jpg . . .]
​
import android.content.Context;
import com.leo.sample.lib.R;
​
public class TestUtils {

    public static int getImageIdByReference() {
        return R.drawable.test;
    }
​
    public static int getImageIdByCode(Context context) {
        return getDrawableId(context, "test");
    }

    private static int getDrawableId(Context context, String name) {
        return getId(context, name, "drawable");
    }

    private static int getId(Context context, String name, String type) {
        return context.getResources().getIdentifier(name, type, context.getPackageName());
    }
}

file drawable下的test图片

而在Sample中,展示一个Activity,其中有两个Button和两个ImageView,点击Button会分别调用这两个SampleLib的接口,并展示图片。

布局代码很简单,就不贴了,下面是Activity的代码:

package com.leo.sample;
​
import com.leo.sample.R;
import com.leo.sample.lib.TestUtils;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
​
public class MainActivity extends Activity {
​
    ImageView ivLogo1;
    ImageView ivLogo2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ivLogo1 = (ImageView) findViewById(R.id.iv_logo1);
        ivLogo2 = (ImageView) findViewById(R.id.iv_logo2);
    }

    public void getImageByRef(View view) {
        int imageId = TestUtils.getImageIdByReference();
        Log.i("TEST", "id = " + "0x" + Integer.toHexString(imageId));
        ivLogo1.setImageResource(imageId);
    }

    public void getImageByCode(View view) {
        int imageId = TestUtils.getImageIdByCode(this);
        Log.i("TEST", "id = " + "0x" + Integer.toHexString(imageId));
        ivLogo2.setImageResource(imageId);
    }
}

好,我们先来试下直接引用。

file

运行App,分别点击两个按钮,效果如下图:

file

可以看到,两个接口都没问题,都能正确的获取到图片的id。好了,再试下导入jar包的方式接入,我们先来导出SampleLib的jar包,只勾选导入src的代码:

file

导入jar包

记得不要忘了把SampleLib下drawable的test图片也复制到Sample项目下,再运行App,分别点击两个按钮,当点击第一个按钮,也就是调用了 “TestUtils.getImageIdByReference()” 闪退了!

原因相信很多小伙伴都知道,因为 “TestUtils.getImageIdByReference()” 接口中直接引用了 ”R.drawable.test“,而资源的id声明是在R.java中,而R.java是在gen文件夹中对应的包名目录下自动生成的:

file

所以调用接口时,找不到对应的资源id,就闪退了。那为什么刚才直接引用SampleLib时,Sample能正常运行呢?

原来,当直接引用时,会在Sample的gen文件夹下,自动生成所引用的项目的R.java,并且会把引用项目的R.java的资源id插入到当前项目的R.java中:

file

自动生成引用项目R.java

file

自动插入引用项目资源id

问题定位到了,我们在导出SampleLib的jar包时把gen文件夹也一起导出:

file 导入jar包(包含gen)

把Sample的jar包换下,重新运行App,分别点击两个按钮。咦,没闪退了,但为什么第一张图片显示了App图标的图片:

file

导入jar包运行结果

看下我打印的图片id值的日志:

file

两种方式获取的id值不相等

咦!两个id值不一样!!分别打开看看Sample的R.java文件和SampleLib的R.java文件:

// Sample R.java
package com.leo.sample;
​
public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static final int ic_launcher=0x7f020000;
        public static final int test=0x7f020001;
    }
    public static final class id {
        public static final int iv_logo1=0x7f060000;
        public static final int iv_logo2=0x7f060001;
    }
    public static final class layout {
        public static final int activity_main=0x7f030000;
    }
    public static final class string {
        public static final int app_name=0x7f040000;
        public static final int hello_world=0x7f040001;
    }
    public static final class style {
        public static final int AppBaseTheme=0x7f050000;
        public static final int AppTheme=0x7f050001;
    }
}
// SampleLib R.java
package com.leo.sample.lib;
​
public final class R {
    public static final class attr {
    }
    public static final class drawable {
        public static int test=0x7f020000;
    }
}

原来如此!

导入jar包时,在项目中不会自动生成所导入jar包的R.java,jar包包含的资源id也不会插入到当前项目的R.java中,而我们是通过复制SampleLib的图片资源“test"到Sample中,所以在Sample的R.java也会有图片资源的id声明,但这个id值并不一定跟SampleLib的R.java中声明的图片资源id值一致(或者说绝大多数情况下都不一致),当拿到id值时,会到当前项目的R.java去索引,因为对应的资源不一致,从而导致检索资源时发生不可预知的错误。

但为什么通过代码获取资源id值就不会出错呢?

因为代码获取资源id是通过资源名的,这时会直接在当前项目(Sample)的R.java文件中检索,所以获取到的资源id值肯定正确。 另外可以看到,SampleLib的R.java中图片“test”的资源id值和Sample的R.java中的App图标“ic_launcher”的资源id值一致,所以第一张图片显示了App图标图片。另外需要注意的是,demo中刚好对应SampleLib的R.java中图片“test”的资源id值是一张图片,如果是一个Button或其他非图片的资源id值,这时App就会闪退了。

结论1:开发SDK时,应该用代码获取资源id,而不是直接引用R。

public static int getId(Context context, String name, String type) {
    return context.getResources().getIdentifier(name, type, context.getPackageName());
}

结论2:接入SDK者在条件允许的情况下,可以选择直接引用的方式接入第三方SDK,这么一来,即使SDK开发者没注意直接引用了R,接入也不会有问题。

那在哪些情况下不允许直接引用第三方SDK呢?

例如,像我,更喜欢复制资源,导入jar包的方式,这样项目结构会更清晰;再例如,有时只用到第三方SDK的部分功能,并不需导入全部jar包,而导致Apk包过大。在这些情况下我们就会导入jar包的方式来接入。但如果第三方SDK开发者没注意我上面所说的问题,在代码中对R资源直接引用了,怎么办?别慌,方法总是有的!

我们可以新建一个项目,把包名声明为跟第三方SDK声明的包名一致,然后复制使用到的jar包、资源和布局等,再在Apk项目中直接引用这个新建的项目。这样一来,第三方SDK即能索引到资源id,又能正确获取到资源id值!

结论3:在SDK直接引用R资源又不想直接引用SDK项目时,可以新建一个与SDK同包名的项目,导入SDK的jar包、资源和必要文件,然后Apk项目直接引用这新建的项目,从而解决问题。

扩展

1. 在Android Studio上的表现 整篇文章都是在Eclipse上测试的,而现在基本绝大多数的Android开发者都转到Android Studio上了,所以这篇文章没什么价值? 其实,经我测试,在Android Studio上原理一样的,只是生成R.java的目录不一样而已,在Android Studio上是在 “../build/generated/source/r/${pakeage}/” 目录下生成的。

另外,经测试,如果项目直接导入aar包,也会在项目中生成arr的R.java文件,并插入到当前项目的R.java中。也就是相当于在Eclipse上直接引用SDK项目。所以不会出现资源索引错误问题。

2. 二次打包资源引用

二次打包的情况下可能很多小伙伴在实际开发中很少接触。什么是二次打包?二次打包就是会对Apk进行一些修改或插入资源和功能,如修改包名、签名,或插入登录模块等,然后再进行编译打包。

因为要插入资源,所以用重新进行一次aapt,对插入的资源重新生成R.java和资源索引表resource.arsc文件。而在aapt后,重新生成R.java中的资源id值可能会变了,如果代码中是直接通过引用R获取资源id的,就会检索到错误的资源。

拿上面的demo举例,假如,SampleLib我们通过直接引用的方式导入库,没二次打包前,资源索引是一一对应的,没有任何问题。而二次打包后,Sample的R.java重新生成,其中的资源id值会改变,这样就跟SampleLib的R.java中的资源id值不对应了,所以就会导致资源检索错误。

这种情况如何解决呢?方法总是有的!

很简单,通过上面的分析,我们知道,直接引用R资源时,程序会先到R的包名下(如SampleLib:com.leo.sample.lib.R)去检索,获取到资源id值,然后通过资源id值去资源索引表resource.arsc文件获取正确的资源文件。

而资源检索表resource.arsc对应的是Sample的R.java,程序发生错误的步骤是在于SampleLib的R.java和Sample的R.java中资源id值不一致!

嘻嘻,说到这里,应该懂了吧?

是的,直接把Sample的R.java文件复制覆盖掉SampleLib的R.java文件,并把包名改成SampleLib的包名!哈哈,这样两个R.java的资源id值就一一对应了。

可能有小伙伴会疑问怎么对R.java文件进行操作。因为二次打包会对Apk先进行反编译,对Apk进行修改后,再对Apk进行编译打包,这样我们就能对Apk的文件进行操作了,而aapt也在这个过程中。对于不了解Apk编译打包的小伙伴,可以搜下“Android打包过程”来了解下,因为这不是本篇文章的讨论范围,就不展开了。

通过上面的分析,我们对R资源引用有了一定的了解,以后在开发中遇到R资源引用问题就能自己解决。上面说了这么多,需要重点强调的是:

开发者在开发SDK时,用代码获取资源id,就万事大吉了!

文源网络,仅供学习之用,如有侵权,联系删除。

我将优质的技术文章和经验总结都汇集在了我的公众号【Java圈子】里。

为方便大家学习,我整理了一套学习资料,涵盖Java虚拟机、spring框架、Java线程、数据结构、设计模式等等,免费提供给热爱Java的同学!

file