1. 程式人生 > >Android R.java類的手動生成

Android R.java類的手動生成

Android中的資源和R.java類

在Android專案中的res目錄中包含了專案使用的各種資源,這些資源全部都分佈在res目錄下的各個子目錄中。每個資源都有兩個屬性,一個是資源的名字,一個是資源的型別。此外,res目錄下的資源在編譯後都會有一個對應的id。

R.java類(以下簡稱R類)是Android中一個非常重要的類,其中定義了res目錄中全部資源的id。在程式碼中通過R類獲取到資源的id後,即可呼叫Android API來獲取和使用對應的資源。例如:

ImageView imageView = (ImageView)findViewById(R.id.imageView);
TextView textView = (TextView)findViewById(R.id.textView); imageView.setImageResource(R.drawable.bg) textView.setText(R.string.app_name)

R類的生成

R類並不包含在專案程式碼中,而是由Android SDK在編譯階段通過aapt工具生成的。一般情況下,開發者不需要關注R類的生成,直接在程式碼中使用即可。然而在某些情況下需要由開發者手動生成R類,並放到專案程式碼中。

為什麼需要手動生成R.java類

Android library

Android 專案根據用途的不同分為app專案和library專案。app專案用來生成可以在Android系統上執行的apk程式,提交到應用市場給使用者使用。而library專案並不會生成apk,而是生成一個sdk,提供給其他開發者使用。

對library專案來說,如果library工程中包含了資源,如layout,drawable,string,dimen等,那麼需要將這些資原始檔和編譯後的程式碼一起放到sdk中。對Eclipse library工程,是將整個res目錄原樣放到sdk中。對Android Studio的library工程,會將整個res目錄打包到aar檔案中。

Android library工程中的R類

對包含資源的library工程來說,和app工程一樣,需要在程式碼中通過資源id來獲取對應的資源。同樣可以在library工程程式碼中通過library工程包名下的R類來獲取對應資源的id。但是library工程中的R類並不會包含在library工程編譯後的jar包或arr包中,也就是說,library工程開發完成後,提供給其他開發者的sdk中並沒有包含這個R類。這個R類同樣是在app工程編譯時生成的。那麼app工程在編譯時是如何知道需要生成library工程包名下的R類呢?

一個app工程可以包含多個library工程(在Android Studio中稱為module)。當一個app工程在編譯時,會遍歷其所引用的所有library工程,生成各個library工程對應的R.java類。這裡會用到每個library sdk中提供的另外一個檔案AndroidManifest.xml。在library工程的AndroidManifest.xml檔案中包含了各個library工程的包名稱,通過這個檔案就可以知道各個library工程的包名,從而生成各個library工程包名下的R類。

例如,一個被引用的library工程中的AndroidManifest.xml檔案中配置的package=”com.cclink.mylib”,則app工程在編譯時會為該library工程生成一個com.cclink.mylib.R的類。

最後app工程中全部java程式碼,app工程的R類,所有library工程的R.java類,都會被編譯,並和所有library工程的jar包合併到一起,然後再轉換成dex檔案。

Android library sdk的特殊使用方式

一個library工程開發完成後會以sdk的形式提供給其他開發者來使用。如前所述,其他開發者在開發app時,如果通過引用的方式來使用該library sdk,則app工程編譯時會自動為該library sdk下的資源生成一個對應的R類,並打包到apk中。
然後,app的開發者並不總是希望通過在Android Studio中新增工程引用的方式來使用一個library sdk。他們有時需要將library sdk中的jar和res直接拷貝到app工程來使用,有時需要將所有依賴的sdk中的jar和res拷貝到一起,合併成一個library來使用。
這樣做有時是為了方便,減少工作量,有時則是沒有辦法通過工程引用方式來使用library。這在遊戲開發時非常常見。很多遊戲引擎只提供了一個目錄,用來放置各類第三方的Android外掛,根本就找不到一個Android的主工程,來建立引用關係。APK的編譯打包也是在遊戲引擎中封裝好了,並不需要通過Eclipse或Android Studio來打包。
例如,Unity3D中通常都是將Android外掛(也就是Android library工程對應的sdk)直接拷貝到/Assets/Plugins/Android目錄中來使用,雖然Unity3D提供了匯出Android工程的功能,但使用起來非常麻煩。開發者很可能不希望這樣去做。

Android library sdk特殊使用方式帶來的問題

如果將sdk中的檔案拷貝到app工程中來使用,或者合併所有的sdk到一個library中,由於這時所有library都混合在一起,app在編譯時根本就找不到原先各個library中的AndroidManifest.xml檔案,也就不會為各個library生成對應的R.java類。當app在執行時,library中程式碼由於找不到對應的R類,會出現ClassNotFound的異常。Unity雖然支援各個sdk分開存放,不需要將sdk合併到一起,但是Unity編譯生成的apk檔案中不會包含任何R類,即使是app包名下的R類也不會生成。

解決沒有R類帶來的問題

要解決沒有R類的問題,需要從兩個角度來考慮。

從library開發者的角度來看,需要讓library工程編譯後的sdk在這種沒有R類情況下能夠正常使用。為了達到此目的,需要將程式碼中所有通過包名下R類來獲取資源id的方式替換為其他的實現方式。Android SDK提供了getResources().getIdentifier()方法來獲取資源id,library開發者可以手動將程式碼中所有需要使用資源id的地方用這種方式來獲取。但getResources().getIdentifier()的引數是資源的名字,是一個字串,這樣在獲取資源id時,就不能使用IDE的程式碼提示,自動補全等功能了。

從app開發者的角度來看,如果需要使用的sdk沒有對這種使用方式做相容,而且不能通過引用方式來使用該sdk時,需要能夠讓該sdk打包到apk後能夠正常執行。為了達到此目錄,需要為該sdk生成一個該sdk包名下的R類,然後將該R類編譯打包到apk中。這樣sdk中的程式碼在執行就不會找不到對應的R類了。

AndroidRClassGenerator工具

AndroidRClassGenerator是一個用來生成R.java類的Python指令碼,基於Python2.7版本。它可以以兩種方式生成R.java類。工具下載地址附在文章最後。
對library開發者來說,AndroidRClassGenerator能夠生成一個新的R類,新的R類中使用了不同的方式來獲取資源id,同時提供了和原先R類一樣的資源訪問的方式。因此,library開發者可以像開發app一樣使用R類來獲取資源,只需要將程式碼中原先的import R類的資訊替換替換為新的R類即可訪問到對應的資源id。AndroidRClassGenerator可同時生成R類,並完成程式碼中import資訊的替換。
對app開發者來說,AndroidRClassGenerator可以根據library工程中的res目錄,和指定的包名來生成對應的R類。

引數配置

在使用前需要先配置config.ini,config.ini中各個引數的含義如下。

  1. ProjectOrResDir:表示library工程的路徑(可以是Eclipse的工程,也可以是Android Studio的工程)。也可以直接指定到某個資源目錄。
  2. sdkdir:Android SDK的路徑
  3. RClassPackage:要生成的目標R類的包名
  4. ReplaceCode:是否替換程式碼中的import資訊,true表示替換,false表示不替換。
    例如library工程的包名為x.y.z,RClassPackage為a.b.c,則當ReplaceCode為true時除了會生成一個a.b.c.R.java類外,還會自動將程式碼中所有的import x.y.z.R,改為import a.b.c。然程式碼通過新生成的R類來獲取資源id。
    只有ProjectOrResDir配置的library工程的路徑時此引數才有效,當ProjectOrResDir配置的是資源目錄時,此項配置會被忽略。

library開發者的使用方法

對library開發者來說,需要生成一個包含指定包名的R類放到程式碼中,然後將其他程式碼中所有用到預設R類(即該library工程包名下R類)的地方都替換為生成的R類。
具體配置流程如下。

  1. 配置ProjectOrResDir為工程的路徑,可以是Eclipse的工程,也可以是Android Studio的工程。
  2. 配置sdkdir為本地的Android SDK的路徑
  3. 配置RClassPackage為需要生成的R類的包名,此包名不可和library的包名完全相同。以免在工程編譯時出現相同類名的編譯錯誤。
  4. ReplaceCode一般應配置為true。如果已經替換過一次,且沒有新的程式碼需要替換,可以設定為false。
  5. 執行RClassGenerator.py
  6. 在library工程入口程式碼處(例如初始化,或者第一個介面顯示時)呼叫新生成R類的R.init(context)方法,context是某個Context物件。

app開發者的使用方法

對app開發者來說,需要生成一個包含library包名的R類放到app工程的程式碼中。
具體配置流程如下。

  1. 如果是Eclipse或完整的Android Studio的library工程,配置ProjectOrResDir為library工程中res目錄的路徑,如果是單獨的aar檔案,需要先將res目錄從aar檔案中解壓出來,然後配置ProjectOrResDir為解壓後的res目錄路徑。
  2. 配置sdkdir為本地的Android SDK的路徑。
  3. 配置RClassPackage為library的包名,library的包名可以在library工程,或aar壓縮包中的AndroidManifest.xml檔案中找到。
  4. ReplaceCode引數在ProjectOrResDir配置為資源目錄時會被自動忽略。這裡不需要配置。
  5. 執行RClassGenerator.py。
  6. 生成的R.java類在res目錄的同級目錄中,將生成的R類拷貝到app專案中,這可能是一個app工程,也可能是工程中某個可以編輯java程式碼的目標,有時甚至需要手動將R類編譯成class並打包成jar放到app工程中。
  7. 在app工程某個入口處呼叫新生成R類的R.init(context)方法,context是某個Context物件。這可能是在主Activity的onCreate()中呼叫,也可能是通過jni方式呼叫。

實現原理

通過用AndroidRClassGenerator生成R類代替了原本應該在app編譯時生成的R類,用這種方式來讓library中程式碼能夠正確的得到資源id。由於資源id只有在apk編譯時才能最終確定,所以AndroidRClassGenerator生成的R類不能用某次編譯時的常量來代替。
有兩種方式可以獲取到資源id:一種是通過反射app包名下的R類來獲取,一種是通過context.getResources().getIdentifier()方法來獲取。
通過反射方式來獲取資源id的原理是,雖然app編譯時不會為每個library生成單獨的R類,但是仍然會生成一個app包名下的R類,這個R類中包含了所有library中的資源id(當然也還包含app自身的全部資源的id),通過反射讀取這個R類,可以得到對應的資源id。
context.getResources().getIdentifier()是android sdk提供的一個api,通過此介面可以獲取到指定型別,指定名稱的資源id。
AndroidRClassGenerator生成的R類同時包含了這兩種實現方式,它首先會嘗試通過反射方式來獲取系統資源id,如果反射方式獲取失敗,再嘗試用context.getResources().getIdentifier()方式來獲取。
之所以先用反射方式來獲取資源id,是因為反射方式比context.getResources().getIdentifier()方式要快得多。但反射方式的缺點是必須要存在一個app包名下的R類。如果這個類不存在(例如,通過Unity3D直接編譯出的apk中就不包含R類),則反射方式失效。
無論有沒有app包名下的R類,context.getResources().getIdentifier()都能夠獲取到資源id,但context.getResources().getIdentifier()方式比反射方式要慢一些。

Android Studio的打包問題

Android Studio從某個版本(具體哪個版本沒有去考究)開始,在編譯時會自動刪除專案中的R.class類,這樣在執行時會出現找不到R類的問題。這時只需要在指令碼中將R.java類改為一個其他的名字就可以了。也就是將”public final class R”改為”public final class XXXR”,同時將destRClassFile = os.path.join(filePath, ‘R.java’)改為destRClassFile = os.path.join(filePath, ‘XXXR.java’)。