寫在前面
我們都知道镣丑,在Android打包時(shí)進(jìn)行的aapt是對(duì)Android的資源打包,從而生成一個(gè)R.java文件汗贫,其中包括非assets下的所有資源的id值司蔬,同時(shí)還生成resource.arsc文件沐序,也就是資源索引表琉用。而我們程序在運(yùn)行中,就是從R.java獲取具體的id值策幼,再到resource.arsc檢索獲取相對(duì)應(yīng)的資源邑时。這文章不是講Android資源打包的過(guò)程,這在老羅的 Android應(yīng)用程序資源的編譯和打包過(guò)程分析 一文講的非常詳細(xì)(反正我看得暈頭轉(zhuǎn)向)特姐,而是講我們平時(shí)開(kāi)發(fā)中對(duì)Android資源引用的幾點(diǎn)注意事項(xiàng)晶丘。
由于個(gè)人的電腦渣渣和對(duì)Eclipse的項(xiàng)目結(jié)構(gòu)更了解,所以我本篇文章的demo是在Eclipse上編寫的。但Eclipse上和Android Studio上資源引用是一樣的浅浮,只是R.java的目錄變了沫浆,后面也會(huì)涉及到Android Studio上的一些問(wèn)題。
正文
下面講述對(duì)R資源引用的一些個(gè)人結(jié)論滚秩,也是我曾經(jīng)踩過(guò)的坑专执,對(duì)于SDK開(kāi)發(fā)的小伙伴或經(jīng)常接入第三方SDK的小伙伴可能會(huì)比較了解,希望對(duì)大家有所幫助和借鑒郁油。
先說(shuō)下demo的結(jié)構(gòu):demo有兩個(gè)項(xiàng)目本股,一個(gè)Sample和一個(gè)SampleLib,Sample直接引用SampleLib或?qū)隨ampleLib的jar包桐腌,從而使用SampleLib里所定義好的接口痊末。由于這個(gè)demo只是為了測(cè)試R資源引用的注意事項(xiàng),所以demo非常簡(jiǎn)單哩掺。在SampleLib只有一個(gè)類,類中只包含兩個(gè)接口涩笤,分別是通過(guò)直接引用R和代碼方式返回一張?jiān)赿rawable里的圖片“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中嚼吞,展示一個(gè)Activity,其中有兩個(gè)Button和兩個(gè)ImageView蹬碧,點(diǎn)擊Button會(huì)分別調(diào)用這兩個(gè)SampleLib的接口舱禽,并展示圖片。布局代碼很簡(jiǎn)單恩沽,就不貼了誊稚,下面是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);
}
}
好,我們先來(lái)試下直接引用罗心。
運(yùn)行App里伯,分別點(diǎn)擊兩個(gè)按鈕,效果如下圖:
可以看到渤闷,兩個(gè)接口都沒(méi)問(wèn)題疾瓮,都能正確的獲取到圖片的id。好了飒箭,再試下導(dǎo)入jar包的方式接入狼电,我們先來(lái)導(dǎo)出SampleLib的jar包,只勾選導(dǎo)入src的代碼:
記得不要忘了把SampleLib下drawable的test圖片也復(fù)制到Sample項(xiàng)目下弦蹂,再運(yùn)行App肩碟,分別點(diǎn)擊兩個(gè)按鈕,當(dāng)點(diǎn)擊第一個(gè)按鈕凸椿,也就是調(diào)用了 “TestUtils.getImageIdByReference()” 閃退了削祈!原因相信很多小伙伴都知道,因?yàn)?“TestUtils.getImageIdByReference()” 接口中直接引用了 ”R.drawable.test“削饵,而資源的id聲明是在R.java中岩瘦,而R.java是在gen文件夾中對(duì)應(yīng)的包名目錄下自動(dòng)生成的:
所以調(diào)用接口時(shí)未巫,找不到對(duì)應(yīng)的資源id,就閃退了启昧。那為什么剛才直接引用SampleLib時(shí)叙凡,Sample能正常運(yùn)行呢?原來(lái)密末,當(dāng)直接引用時(shí)握爷,會(huì)在Sample的gen文件夾下,自動(dòng)生成所引用的項(xiàng)目的R.java严里,并且會(huì)把引用項(xiàng)目的R.java的資源id插入到當(dāng)前項(xiàng)目的R.java中:
問(wèn)題定位到了新啼,我們?cè)趯?dǎo)出SampleLib的jar包時(shí)把gen文件夾也一起導(dǎo)出:
把Sample的jar包換下,重新運(yùn)行App刹碾,分別點(diǎn)擊兩個(gè)按鈕燥撞。咦,沒(méi)閃退了迷帜,但為什么第一張圖片顯示了App圖標(biāo)的圖片:
看下我打印的圖片id值的日志:
咦物舒!兩個(gè)id值不一樣!戏锹!分別打開(kāi)看看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;
}
}
原來(lái)如此冠胯!導(dǎo)入jar包時(shí),在項(xiàng)目中不會(huì)自動(dòng)生成所導(dǎo)入jar包的R.java锦针,jar包包含的資源id也不會(huì)插入到當(dāng)前項(xiàng)目的R.java中荠察,而我們是通過(guò)復(fù)制SampleLib的圖片資源“test"到Sample中,所以在Sample的R.java也會(huì)有圖片資源的id聲明奈搜,但這個(gè)id值并不一定跟SampleLib的R.java中聲明的圖片資源id值一致(或者說(shuō)絕大多數(shù)情況下都不一致)悉盆,當(dāng)拿到id值時(shí),會(huì)到當(dāng)前項(xiàng)目的R.java去索引馋吗,因?yàn)閷?duì)應(yīng)的資源不一致舀瓢,從而導(dǎo)致檢索資源時(shí)發(fā)生不可預(yù)知的錯(cuò)誤。
但為什么通過(guò)代碼獲取資源id值就不會(huì)出錯(cuò)呢耗美?因?yàn)榇a獲取資源id是通過(guò)資源名的京髓,這時(shí)會(huì)直接在當(dāng)前項(xiàng)目(Sample)的R.java文件中檢索,所以獲取到的資源id值肯定正確商架。
另外可以看到堰怨,SampleLib的R.java中圖片“test”的資源id值和Sample的R.java中的App圖標(biāo)“ic_launcher”的資源id值一致,所以第一張圖片顯示了App圖標(biāo)圖片蛇摸。另外需要注意的是备图,demo中剛好對(duì)應(yīng)SampleLib的R.java中圖片“test”的資源id值是一張圖片,如果是一個(gè)Button或其他非圖片的資源id值,這時(shí)App就會(huì)閃退了揽涮。
結(jié)論1:開(kāi)發(fā)SDK時(shí)抠藕,應(yīng)該用代碼獲取資源id,而不是直接引用R蒋困。
public static int getId(Context context, String name, String type) {
return context.getResources().getIdentifier(name, type, context.getPackageName());
}
結(jié)論2:接入SDK者在條件允許的情況下盾似,可以選擇直接引用的方式接入第三方SDK,這么一來(lái)雪标,即使SDK開(kāi)發(fā)者沒(méi)注意直接引用了R零院,接入也不會(huì)有問(wèn)題。
那在哪些情況下不允許直接引用第三方SDK呢村刨?例如告抄,像我,更喜歡復(fù)制資源嵌牺,導(dǎo)入jar包的方式打洼,這樣項(xiàng)目結(jié)構(gòu)會(huì)更清晰;再例如逆粹,有時(shí)只用到第三方SDK的部分功能拟蜻,并不需導(dǎo)入全部jar包,而導(dǎo)致Apk包過(guò)大枯饿。在這些情況下我們就會(huì)導(dǎo)入jar包的方式來(lái)接入。但如果第三方SDK開(kāi)發(fā)者沒(méi)注意我上面所說(shuō)的問(wèn)題诡必,在代碼中對(duì)R資源直接引用了奢方,怎么辦?別慌爸舒,方法總是有的蟋字!
我們可以新建一個(gè)項(xiàng)目,把包名聲明為跟第三方SDK聲明的包名一致扭勉,然后復(fù)制使用到的jar包鹊奖、資源和布局等,再在Apk項(xiàng)目中直接引用這個(gè)新建的項(xiàng)目涂炎。這樣一來(lái)忠聚,第三方SDK即能索引到資源id,又能正確獲取到資源id值唱捣!
結(jié)論3:在SDK直接引用R資源又不想直接引用SDK項(xiàng)目時(shí)两蟀,可以新建一個(gè)與SDK同包名的項(xiàng)目,導(dǎo)入SDK的jar包震缭、資源和必要文件赂毯,然后Apk項(xiàng)目直接引用這新建的項(xiàng)目,從而解決問(wèn)題。
擴(kuò)展
1. 在Android Studio上的表現(xiàn)
整篇文章都是在Eclipse上測(cè)試的党涕,而現(xiàn)在基本絕大多數(shù)的Android開(kāi)發(fā)者都轉(zhuǎn)到Android Studio上了烦感,所以這篇文章沒(méi)什么價(jià)值?其實(shí)膛堤,經(jīng)我測(cè)試手趣,在Android Studio上原理一樣的,只是生成R.java的目錄不一樣而已骑祟,在Android Studio上是在 “../build/generated/source/r/${pakeage}/” 目錄下生成的回懦。
另外,經(jīng)測(cè)試次企,如果項(xiàng)目直接導(dǎo)入aar包怯晕,也會(huì)在項(xiàng)目中生成arr的R.java文件,并插入到當(dāng)前項(xiàng)目的R.java中缸棵。也就是相當(dāng)于在Eclipse上直接引用SDK項(xiàng)目舟茶。所以不會(huì)出現(xiàn)資源索引錯(cuò)誤問(wèn)題。
2. 二次打包資源引用
二次打包的情況下可能很多小伙伴在實(shí)際開(kāi)發(fā)中很少接觸堵第。什么是二次打包吧凉?二次打包就是會(huì)對(duì)Apk進(jìn)行一些修改或插入資源和功能,如修改包名踏志、簽名阀捅,或插入登錄模塊等,然后再進(jìn)行編譯打包针余。因?yàn)橐迦胭Y源饲鄙,所以用重新進(jìn)行一次aapt,對(duì)插入的資源重新生成R.java和資源索引表resource.arsc文件圆雁。而在aapt后忍级,重新生成R.java中的資源id值可能會(huì)變了,如果代碼中是直接通過(guò)引用R獲取資源id的伪朽,就會(huì)檢索到錯(cuò)誤的資源轴咱。
拿上面的demo舉例,假如烈涮,SampleLib我們通過(guò)直接引用的方式導(dǎo)入庫(kù)朴肺,沒(méi)二次打包前,資源索引是一一對(duì)應(yīng)的坚洽,沒(méi)有任何問(wèn)題宇挫。而二次打包后,Sample的R.java重新生成酪术,其中的資源id值會(huì)改變器瘪,這樣就跟SampleLib的R.java中的資源id值不對(duì)應(yīng)了翠储,所以就會(huì)導(dǎo)致資源檢索錯(cuò)誤娘锁。
這種情況如何解決呢野宜?方法總是有的!很簡(jiǎn)單队贱,通過(guò)上面的分析欣除,我們知道住拭,直接引用R資源時(shí),程序會(huì)先到R的包名下(如SampleLib:com.leo.sample.lib.R)去檢索历帚,獲取到資源id值滔岳,然后通過(guò)資源id值去資源索引表resource.arsc文件獲取正確的資源文件。而資源檢索表resource.arsc對(duì)應(yīng)的是Sample的R.java挽牢,程序發(fā)生錯(cuò)誤的步驟是在于SampleLib的R.java和Sample的R.java中資源id值不一致谱煤!嘻嘻,說(shuō)到這里禽拔,應(yīng)該懂了吧刘离?是的,直接把Sample的R.java文件復(fù)制覆蓋掉SampleLib的R.java文件睹栖,并把包名改成SampleLib的包名硫惕!哈哈,這樣兩個(gè)R.java的資源id值就一一對(duì)應(yīng)了野来。
可能有小伙伴會(huì)疑問(wèn)怎么對(duì)R.java文件進(jìn)行操作恼除。因?yàn)槎未虬鼤?huì)對(duì)Apk先進(jìn)行反編譯,對(duì)Apk進(jìn)行修改后曼氛,再對(duì)Apk進(jìn)行編譯打包豁辉,這樣我們就能對(duì)Apk的文件進(jìn)行操作了,而aapt也在這個(gè)過(guò)程中搪锣。對(duì)于不了解Apk編譯打包的小伙伴,可以搜下“Android打包過(guò)程”來(lái)了解下彩掐,因?yàn)檫@不是本篇文章的討論范圍构舟,就不展開(kāi)了。
寫在最后
通過(guò)上面的分析堵幽,我們對(duì)R資源引用有了一定的了解狗超,以后在開(kāi)發(fā)中遇到R資源引用問(wèn)題就能自己解決。上面說(shuō)了這么多朴下,需要重點(diǎn)強(qiáng)調(diào)的是:
開(kāi)發(fā)者在開(kāi)發(fā)SDK時(shí)努咐,用代碼獲取資源id,就萬(wàn)事大吉了E闺省渗稍!