寫在前面
我們都知道,在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());
}
}
而在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);
}
}
好,我們先來試下直接引用。
運行App,分別點擊兩個按鈕,效果如下圖:
可以看到,兩個接口都沒問題,都能正確的獲取到圖片的id。好了,再試下導入jar包的方式接入,我們先來導出SampleLib的jar包,只勾選導入src的代碼:
記得不要忘了把SampleLib下drawable的test圖片也復制到Sample項目下,再運行App,分別點擊兩個按鈕,當點擊第一個按鈕,也就是調用了 “TestUtils.getImageIdByReference()” 閃退了!原因相信很多小伙伴都知道,因為 “TestUtils.getImageIdByReference()” 接口中直接引用了 ”R.drawable.test“,而資源的id聲明是在R.java中,而R.java是在gen文件夾中對應的包名目錄下自動生成的:
所以調用接口時,找不到對應的資源id,就閃退了。那為什么剛才直接引用SampleLib時,Sample能正常運行呢?原來,當直接引用時,會在Sample的gen文件夾下,自動生成所引用的項目的R.java,并且會把引用項目的R.java的資源id插入到當前項目的R.java中:
問題定位到了,我們在導出SampleLib的jar包時把gen文件夾也一起導出:
把Sample的jar包換下,重新運行App,分別點擊兩個按鈕。咦,沒閃退了,但為什么第一張圖片顯示了App圖標的圖片:
看下我打印的圖片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,就萬事大吉了!!