Android開發中R資源引用注意事項

寫在前面

我們都知道,在Android打包時進行的aapt是對Android的資源打包,從而生成一個R.java文件,其中包括非assets下的所有資源的id值,同時還生成resource.arsc文件,也就是資源索引表。而我們程序在運行中,就是從R.java獲取具體的id值,再到resource.arsc檢索獲取相對應的資源。這文章不是講Android資源打包的過程,這在老羅的 Android應用程序資源的編譯和打包過程分析 一文講的非常詳細(反正我看得暈頭轉向),而是講我們平時開發中對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());
    }
}
drawable下的test圖片.jpg

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

好,我們先來試下直接引用。


直接引用.png

運行App,分別點擊兩個按鈕,效果如下圖:

直接引用項目運行結果.png

可以看到,兩個接口都沒問題,都能正確的獲取到圖片的id。好了,再試下導入jar包的方式接入,我們先來導出SampleLib的jar包,只勾選導入src的代碼:

導入jar包.png

記得不要忘了把SampleLib下drawable的test圖片也復制到Sample項目下,再運行App,分別點擊兩個按鈕,當點擊第一個按鈕,也就是調用了 “TestUtils.getImageIdByReference()” 閃退了!原因相信很多小伙伴都知道,因為 “TestUtils.getImageIdByReference()” 接口中直接引用了 ”R.drawable.test“,而資源的id聲明是在R.java中,而R.java是在gen文件夾中對應的包名目錄下自動生成的:

R.java生成.png

所以調用接口時,找不到對應的資源id,就閃退了。那為什么剛才直接引用SampleLib時,Sample能正常運行呢?原來,當直接引用時,會在Sample的gen文件夾下,自動生成所引用的項目的R.java,并且會把引用項目的R.java的資源id插入到當前項目的R.java中:

自動生成引用項目R.java.png
自動插入引用項目資源id.png

問題定位到了,我們在導出SampleLib的jar包時把gen文件夾也一起導出:

導入jar包(包含gen).png

把Sample的jar包換下,重新運行App,分別點擊兩個按鈕。咦,沒閃退了,但為什么第一張圖片顯示了App圖標的圖片:

導入jar包運行結果.png

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

兩種方式獲取的id值不相等.png

咦!兩個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,就萬事大吉了!!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,581評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,993評論 19 139
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,973評論 6 342
  • 麻衣是日本一個知名博主,自稱“扔東西的變態”,博客名叫“啥都沒有的博客”,所有的網紅都教你買買買,而她卻教你扔扔扔...
    趙黎丹閱讀 557評論 0 1
  • 大家好,我是日記星球217號星寶寶裙子,我正在參加日記星球第五期21天的蛻變之旅,這是我的第一篇原創日記! 年年過...
    環保達人羅群閱讀 358評論 1 4