1. 程式人生 > >Android 以jar包方式共享資源注意事項

Android 以jar包方式共享資源注意事項

最近的一個專案是一個Android系統的系統應用的重構開發,專案中有很多個應用,這些 應用有許多相同的介面和互動;另外,這一套應用的介面可能會需要經常調整來適配不同的客戶需求。為了減少開發和維護的工作量,我把這些應用的資源統一起來 一起維護,相同的資源不需要維護2份,並且適配新資源(圖片、多國語言等)工作量也能做到最小,畢竟,人力資源是有限的。

    為了實現這個功能,我嘗試了使用jar包的方式來共享資源,過程中遇到了一些問題,現在把這些問題歸納成四點,記錄在這裡,希望能幫到跟我有同樣需求的人。這四點分別是:

一. 以lib工程方式靜態共享資源;

二. Android不支援jar包中的資源的訪問;

三. 第三方釋出的開發包帶有資源時的處理方式;

四. 為什麼Android系統資源包Android.jar中的資源可以被訪問。

  本文的Demo程式碼位於http://download.csdn.net/detail/romantic_energy/8598171,  有需要的朋友自己去下載。

一. 以lib工程方式靜態共享資源

    把應用中的所有資源都放到了一個Android lib工程中(project->property->Android選項中把 Is Labrary勾選中),假設這個工程名為ResLib,它包含2個圖片drawable:ok_n.png、ok_d.png , 一個xml drawable:selector_ok.xml ,內容為:

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:drawable="@drawable/ok_p" android:state_pressed="true" />
    <item android:drawable="@drawable/ok_n" />
</selector>

    再建一個apk工程,名為app1,它包含一個圖片drawable ic_launcher.png,一個layout activity_main.xml, 在app1工程的屬性中選擇Android,點選Library選項框的add...按鈕, 選中ResLib作為app1的依賴工程。在

activity_main.xml中增加一個按鈕:

<Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/textView1"
        android:layout_below="@+id/textView1"
        android:layout_marginLeft="41dp"
        android:layout_marginTop="75dp"
        android:text="Button"
android:background="@drawable/selector_ok"/>

按鈕中用到了ResLib中的drawable。

編譯ResLib,然後再編譯app1, 發現沒問題, app1正確引用到了ResLib中的drawableselector_ok

    分別觀察ResLib、app1中自動生成的R.java檔案,發現總共有3個R.java檔案,分別是:

1. 位於ResLib中的com.example.reslib.R.java;

2. 位於app1中的com.example.reslib.R.java;

3. 位於app1中的com.example.app1.R.java;

檢視R.java 中的drawable類,發現

1. 位於ResLib中的com.example.reslib.R.java中的是:

public static final class drawable {
public static int ok_n=0x7f020000;
        public static int ok_p=0x7f020001;
public static int selector_ok=0x7f020002;
    }

2. 位於app1中的com.example.reslib.R.java是:

public static final class drawable {
public static final int ok_n = 0x7f020001;
        public static final int ok_p = 0x7f020002;
public static final int selector_ok = 0x7f020003;
    }

3. 位於app1中的com.example.app1.R.java是:

 public static final class drawable {
public static final int ic_launcher=0x7f020000;
public static final int ok_n=0x7f020001;
        public static final int ok_p=0x7f020002;
public static final int selector_ok=0x7f020003;
    }

這裡有2個需要注意的地方:

A. 變數的型別: 位於ResLib中的R.java中的drawable型別是public static int, 沒有final,說明它不是常量, 可以在執行時變化,從設計上說,不能用這個值來索引資源,因為它可能在執行時被修改; 位於app1中dR.java中的drawable型別public static final int,是常量,可以用來索引資源。

B. 變數的值:以標註為紅色的selector_ok為例,位於ResLib中的R.java中的selector_ok值為0x7f020002位於app1中dR.java中的selector_ok值為0x7f020003; 如果在app1中直接使用位於ResLib中的R.java中的selector_ok來獲取資源,其實獲取的是ok_p, 因為ok_p的值是0x7f020002

    再來把app1工程生成的apk包解壓縮,看看裡面都有些什麼drawable,發現裡面包含了3個圖片drawableic_launcher.pngok_n.png、ok_d.png , 一個xml drawable:selector_ok.xml, 也就是說app1的apk包把所有在ResLib中存在的drawable資源都拷到了apk包中。

    最後我們通過aapt命令來檢視一下app1.apk中的資源索引表,從命令列進入到app1.apk所在的目錄,輸入以下命令:aapt d resources app1.apk > out.txt, 檢視out.txt, 可以看到以下內容:

Package Groups (1)
Package Group 0 id=127 packageCount=1 name=com.example.app1
  Package 0 id=127 name=com.example.app1 typeCount=8
    type 0 configCount=0 entryCount=0
    type 1 configCount=1 entryCount=4
      spec resource 0x7f020000 com.example.app1:drawable/ic_launcher: flags=0x00000000
      spec resource 0x7f020001 com.example.app1:drawable/ok_n: flags=0x00000000
      spec resource 0x7f020002 com.example.app1:drawable/ok_p: flags=0x00000000
      spec resource 0x7f020003 com.example.app1:drawable/selector_ok: flags=0x00000000

    我們看到app1中的資源索引表中是含有ok_nok_p、selector_ok資源的索引的,並且索引值跟app1中的R.java中的值一樣。

由上可知:app1不會直接引用ResLib中的資源,不會使用位於ResLib中的R.java檔案,而是把屬於ResLib中的資源拷貝到了自己的apk中,把這些資源當作自己的資源,再重新生成資源ID和R.java檔案,然後像訪問自己的資源一樣去訪問原本屬於ResLib中的資源。

二. Android不支援jar包中的資源的訪問;

    區別於第一點,這裡的jar包是指不帶原始碼工程的jar包, lib工程預設生成的jar包中是隻包含.class檔案,不帶資原始檔的, 如果你想你的jar包中包含資原始檔,需要使用Eclipse的匯出(export)功能,將資原始檔一起匯出到jar包中,但是請注意:以這種方式匯出的資原始檔不僅引用jar包的apk工程無法使用,連jar包中的程式碼也無法使用。

下面來看看,為什麼Android中jar包中的資原始檔無法訪問。

    先來做個實驗,看看使用使用Eclipse的匯出(export)功能匯出的ResLib中的資源id,與直接工程依賴的ResLib中的id,是否一樣。

    我們在之前的ResLib工程中增加一個ResLibTest.java檔案, 包名com\example\reslib,內容為:

package com.example.reslib;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.Log;
public class ResLibTest {
    public ResLibTest() {        
        Log.i("ResLibTest", "id: ok_n = 0x" + Integer.toHexString(R.drawable.ok_n));
        Log.i("ResLibTest", "id: ok_p = 0x" + Integer.toHexString(R.drawable.ok_p));
        Log.i("ResLibTest", "id: selector_ok = 0x" + Integer.toHexString(R.drawable.selector_ok));
    }   
    public Drawable get_ok_n(Context context) {
        return context.getResources().getDrawable(R.drawable.ok_n);
    }
}

    使用Eclipse中的匯出功能,把ResLib的程式碼和資源匯出到名為ResLib.jar的jar包中,匯出方式是: 右擊ResLib工程->Export->Java->JAR file  點選Next,將ResLib的以下內容匯出:


    注意不要把manifest.xml匯出,否則引用時會報錯:有2個manifest.xml。

    任然在app1中已程式碼工程方式引用ResLib,並在app1的MainActivity類中的onCreate函式中增加以下程式碼:

        ResLibTest test = new ResLibTest();        
        TextView view1 = (TextView)findViewById(R.id.textView1);
        view1.setBackground(test.get_ok_n(this));

    以此檢視資源id和test.get_ok_n()函式返回的drawable。

    再建一個app2的apk工程,只包含ic_launcher.png一個drawable,把之前匯出的ResLib.jar包拷貝到工程的libs目錄下,同樣在app2的MainActivity類中的onCreate函式中增加以下程式碼:

        ResLibTest test = new ResLibTest();        
        TextView view1 = (TextView)findViewById(R.id.textView1);
        view1.setBackground(test.get_ok_n(this));

    以此檢視資源id和test.get_ok_n()函式返回的drawable。   

    執行app1,得到以下列印資訊:

id: ok_n = 0x7f020001
id: ok_p = 0x7f020002
id: selector_ok = 0x7f020003

    並且textView1顯示的背景是ok_n.png。

    執行app2,得到以下列印資訊:

id: ok_n = 0x7f020000
id: ok_p = 0x7f020001
id: selector_ok = 0x7f020002
    並且textView1顯示的背景是ic_launcher.png。   

    由上可知,使用匯出包ResLib.jar時,得到的資源id與直接工程依賴時得到的id並不同,並且使用匯出包ResLib.jar時,訪問到了錯誤的資源。

    Android中一般資源的訪問方式是:    context.getResources().getDrawable(R.drawable.ok_n);

    說明資源是和Context、ResourceManager類關聯在一起的,而匯出的jar包無法構造出類似的關聯類,Android本身也沒有提供類似的訪問機制,所以我們無法以正常的方式來訪問jar包中的資源。

    stackoverflow上有2個問題是關於這個的, 分別是:

http://stackoverflow.com/questions/9087096/packaging-drawable-resources-with-a-jar http://stackoverflow.com/questions/9868546/android-how-to-export-jar-with-resources

    另外,Android文件中也有相關的介紹:http://tools.android.com/recent/buildchangesinrevision14

    但其實jar包中的資原始檔還是可以被訪問到的,只不過是被當作一般的檔案io流,需要自己去解析,這並不是一個完整的方案,會引出許多其它的問題,所以實際意義不大。這個只給出圖片檔案的訪問方式,在ResLibTest類中加入以下函式:

public Drawable get_ok_n_2(Context context) {        
        Bitmap bitmap = null;
        BitmapDrawable drawable;
        InputStream iStream = getClass().getClassLoader()
                .getResourceAsStream("res/drawable/ok_n.png");
        Log.i("ResLibTest", "iStream = " + iStream);
        if(iStream != null) {
            bitmap = BitmapFactory.decodeStream(iStream);
            drawable = new BitmapDrawable(context.getResources(),bitmap);    
            return drawable;
        }
        return null;
    }

匯出ResLib後,使用get_ok_n_2能得到正確的drawable。這裡有3點值得注意:

A. getClass().getClassLoader().getResourceAsStream 和 getClass().getResourceAsStream都能獲取到io流,ClassLoader版本的getResourceAsStream不能訪問"/res/drawable/ok_n.png", 注意前面的斜槓,Class版本的getResourceAsStream可以。並且Class版本的函式呼叫的也是ClassLoader中的函式。

B. 在app2中可以以以下方式訪問圖片:

ResLibTest test = new ResLibTest();   
InputStream iStream = test.getClass().getResourceAsStream("res/drawable/ok_n.png");

C. iStream的列印結果是:

iStream = l[email protected]41ade240

當以getResourceAsStream方式來獲取xml檔案時,xml需要全部自己解析之後再根據解析結果去載入相應的資原始檔,實際應用中不具備實際意義,這裡就不深究了。

getResourceAsStream方式載入的檔案屬於java類載入器提供的功能,Andriod並沒有為獲取jar包中的資源提供任何便利的方法,所以得出的結論是:Android不支援jar包中的資源的訪問

三. 第三方釋出的開發包帶有資源時的處理方式;

一般是類檔案匯出成jar包,和資原始檔分開,一起提供給客戶。客戶端的apk把資源打包到自己的apk中,此時jar包中的類不能再以資源id來訪問資源,而是使用由apk層傳過來的Context物件加上資源路徑來訪問。如下:

public int getDrawableID(Context context, String strPath) {
        return context.getResources().getIdentifier(strPath,
                "drawable", context.getPackageName());
    }

    網上關於這種方式的論述有很多,這裡不再贅述。

四. 為什麼Android系統資源包Android.jar中的資源可以被訪問;

    這個是最困擾我的一個問題。這要從android資源編譯打包、系統資源引用方式方面說起。以下2篇博文對這個問題有些論述:   

Android工程編譯過程: http://www.cnblogs.com/devinzhang/archive/2011/12/20/2294686.html
Android應用程式資源的編譯和打包過程分析
http://blog.csdn.net/luoshengyang/article/details/8744683     首先,來看看 Android.jar中有些什麼內容。用解壓工具開啟Android.jar包,可以看到以下內容:

    這其中包括了Android Framework層的類庫、res中包含了系統資源、android目錄下的R類中包含了系統資源對應的id、resources.arsc是資源索引表。
    Android程式編譯時會先使用AAPT(Android Asset Packaging Tool)資源編譯工具編譯資源,這個工具也能檢視jar包或者apk包中的資源id及其對應的資源名稱的對應關係,事實上這個對應關係儲存在resources.arsc檔案中。現在我們使用AAPT命令來檢視一下android-17中的android.jar包中的資源id索引情況。在命令列中進入到SDK的platforms\android-17目錄下,輸入以下命令 :aapt d resources android.jar > android_jar_res.txt,可以得到一個記錄了id、名字相互索引的android_jar_res.txt檔案,部分內容如下:
Package Groups (1)
Package Group 0 id=1 packageCount=1 name=android
  Package 0 id=1 name=android typeCount=20
    type 0 configCount=1 entryCount=1112
      spec resource 0x01010000 android:attr/theme: flags=0x40000000
      spec resource 0x01010001 android:attr/label: flags=0x40000000
      spec resource 0x01010002 android:attr/icon: flags=0x40000000
      spec resource 0x01010003 android:attr/name: flags=0x40000000
      spec resource 0x01010004 android:attr/manageSpaceActivity: flags=0x40000000
      spec resource 0x01010005 android:attr/allowClearUserData: flags=0x40000000
      spec resource 0x01010006 android:attr/permission: flags=0x40000000
    可見Android.jar包中確實包含了系統資源id及其名字的索引關係。
    Android app編譯時會執行aapt資源編譯命令,使用命令列編譯的命令如下:
aapt p -f -m -J gen -S res -I ~/android-sdk-linux/platforms/android-17/android.jar -M AndroidManifest.xml
    其中的-I命令的解釋是:  -I  add an existing package to base include set, 即新增一個現有的包作為基礎引入包。檢視aapt 的原始碼可知aapt命令執行時會解析這個包,然後就可以使用解析出來的id了,亦即系統資源id!所以在Android app編譯時,app中引用的系統資源id能被識別並正確的引用。

    Android app雖然引用了系統資源,但其apk包中並不包含系統資源拷貝(這點從apk包的大小就可以看出來),而是在執行時載入了系統資源包,從而通過系統資源id訪問到了系統資源。這個系統資源卻並不是Androd.jar,而是:/system/framework/framework-res.apk。這個apk在應用程式啟動時由AssetManager載入,具體載入過程可以檢視老羅的博文: Android應用程式資源管理器(Asset Manager)的建立過程分析 。
    app編譯時根據Android.jar包已經確定好了系統資源id,但是執行時載入的卻是framework-res.apk,所以Android.jar和framework-res.apk應該有某種意義上的對應關係。我們使用adb命令,把位於虛擬機器的/system/framework/framework-res.apk檔案pull到PC上,然後用
aapt d resources framework-res.apk > framework-res.txt
命令得到記錄系統id、名字索引關係的檔案framework-res.txt經過與android.jar包產生的android_jar_res.txt對比發現, 他們的id、名字索引關係是一樣的!可知使用Android.jar中的id在framework-res.apk中不會訪問到錯誤的資源!這也是為什麼所以應用程式即使不包含圖片資源也能顯示美觀的介面,並且同一個app安裝到不同的Android系統中可以表現為不同的形式,因為執行時動態載入嘛!
在此總結一下為什麼Android系統資源包Android.jar中的資源可以被訪問
1. app引入了系統資源,這些系統資源及其id和名字的索引包含在Android.jar包中。
2. app編譯時會執行aapt資源編譯打包命令,aapt資源編譯打包命令的-I 引數,引入了Android.jar,所以app在編譯的時候,系統資源id能被識別。
3. apk包中只包含了對系統資源id的索引,並不包含真正的資源,否則apk包不會那麼小。
4. apk在執行時載入的系統資源其實包含在/system/framework/framework-res.apk包中,這個包的資源索引表跟Android.jar包相同,在apk執行時由framework層中的 AssertManager自動載入,app需要引用系統資源時,通過使用編譯時固定的id到framework-res.apk包中查詢。 所以Android.jar中的資源可以被訪問其實是個假象, app只是應用了位於其中的資源id及索引,這一步是在編譯時就完成了。真正的資源訪問是在執行時framework-res.apk包中查詢的。

相關推薦

Android jar方式共享資源注意事項

最近的一個專案是一個Android系統的系統應用的重構開發,專案中有很多個應用,這些 應用有許多相同的介面和互動;另外,這一套應用的介面可能會需要經常調整來適配不同的客戶需求。為了減少開發和維護的工作

Android apk方式共享資源(動態換膚)的實現方式

一 應用場景: 之前的一個專案是一個Android系統的系統應用的重構開發,專案中有很多個應用,這些 應用有許多相同的介面和互動;另外,這一套應用的介面可能會需要經常調整來適配不同的客戶需求。為了減少開發和維護的工作量,我把這些應用的資源統一起來 一起維護,相同的資源不需要

jar方式啟動

rfi datetime lse ati 工具 vivo cms launcher lips 嗨,大家好~ ,在工作中,一個項目要部署到服務器上,我自己在工作中常見的部署tomcat和jar包方式部署的兩種方式。(其他的有待挖掘) 啊啊啊,公司正在做一個新的平臺,所

SpringBootjar方式在後臺執行

以前web專案都是打成war放在Tomcat等伺服器中執行,springBoot簡化了該流程,它可以直接以jar包的方式執行,因為它裡面內建了Tomcat伺服器。SpringBoot以jar包方式在後臺執行的命令為: java -jar spring-boot01-1.0-SNAPSHO

Spring Boot jar 方式執行在後臺

Spring-boot jar 包方式啟動: 首先,為了防止和常用的 Tomcat 8080 埠衝突,將 Spring-boot 專案的埠號設定為 9090。 具體方法:在 application.properties 檔案裡寫 server.port=90

JAR中MANIFEST.MF注意事項

1. Manifest-Version     用來定義manifest檔案的版本,例如:Manifest-Version: 1.0 2. Created-By     宣告該檔案的生成者,一般該屬性是由jar命令列工具生成的,例如:Created-By: Apache An

將 Spring boot 專案打成可執行Jar,及相關注意事項(main-class、缺少 xsd、重複打包依賴)

最近在看 spring boot 的東西,覺得很方便,很好用。對於一個簡單的REST服務,都不要自己部署Tomcat了,直接在 IDE 裡 run 一個包含 main 函式的主類就可以了。 但是,轉念一想,到了真正需要部署應用的時候,不可能通過 IDE 去部署

Android 封裝jar,反射機制獲取資源

public class ResourceUtil {        public static int getLayoutId(Context paramContext, St

SpringBoot專案搭建並jar方式部署執行

一、專案搭建: 1. 去springBoot官網下載demo    SpringBoot官網:https://start.spring.io/; 2. 點選Generate Project下載demo並將其以maven方式匯入到eclipse中; 3. 選擇要整合的功能框架如

Android studio jar生成方式

Right im not sure if it will work for others but worked for me. I changed proxyPort to 8080 and used jcenter instead of Maven. But i had to apply expeptio

Android封裝jar,把當前專案設定成module,封裝手機振動jar給unity呼叫

Android封裝jar包或者把當前專案設定成module操作步驟都一樣,此處以手機振動為例,封裝jar包給Unity平臺使用,具體如下: Android部分: 1.建立unity要呼叫的手機振動的類:PhoneVibrate package com.lin.phonevibrate;

Eclipse中新增Android系統jar

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

idea打jar方式

方式一 使用idea自帶的打包方式 點選File ->project structure    選擇Artifacts 然後點選綠色加號    按圖一次選擇 點選   完成後會彈出此介面  此時需要注意 資訊的填寫十分重要 否則將不能執行 1

jar方式執行web專案檔案上傳和訪問

spingBoot2.x使用 java -jar執行方式的圖片上傳和訪問處理 1.打包成jar包,需要增加maven依賴         <build>        &nb

Linuxjar形式啟動java專案

1、安裝Eclipse打包外掛Fat Jar 用以打jar  Fat Jar Eclipse Plug-In是一個可以將Eclipse Java Project的所有資源打包進一個可執行jar檔案的小

Centos7.2之jenkins從github拉程式碼jar啟動

  1,新建一個job 2,配置git下載地址  3,配置build後執行的指令碼 4,指令碼執行過程會報沒有許可權,因為jenkins預設使用者是jenkins,改成root vi /etc/sysconfig/jenkins 5,指令碼如下: ec

[Android] 解決androidjarR衝突問題

【問題描述】 今天在開發專案時,遇到一個詭異的問題:一個App工程下明明有a.xml檔案,但在OnCreate回撥裡進行 setContentView設定對應的layout時,卻一直顯示找不到對應的資源。開始以為是沒有重新編譯造成衝突引起 的,後來重新編譯了整個工程還是存在

idea maven工程打可執行jar方式

步驟: 1. 利用idea 建立maven工程 2.在Project Setting的Artifacts中module中新建jar,如下圖: ***** 注意:::::上路中的Directory for META-INF/MANIFEST.MF的路徑要選擇工程目錄的s

Android檢視jar原始碼問題

在Eclipse中開發android的應用程式時,有時想檢視函式的內部實現,但是當在函式上點選ctrl和滑鼠左鍵的時候, 往往出現如下提示:  Class File Editor Source not found The JAR of this class fil

Androidjar打包成dex檔案

進入sdk/build-tools/27.0.3目錄,將jar包dex.jar拷貝到該目錄下,在空白處按住shift鍵並點選右鍵,選擇“在此處開啟命令列”輸入命令以下命令(注意其中的空格):dx --