1. 程式人生 > >Android sharedUserId研究記錄 【轉】

Android sharedUserId研究記錄 【轉】

原文地址: 

剛整理完的文件,順便園子發個分享。因工作繁忙,還是有不少方面無法深入測試,歡迎各位回帖提出意見~

簽名簡介:

在Android 系統中,所有安裝到系統的應用程式都必有一個數字證書,此數字證書用於標識應用程式的作者和在應用程式之間建立信任關係,。這個數字證書並不需要權威的數字證書籤名機構認證,它只是用來讓應用程式包自我認證的。

除錯時,ADT會自動的使用debug金鑰為應用程式簽名。debug金鑰是一個名為debug.keystore的檔案,它的位置:系統碟符:/Documents and Settings/XXX/.android/debug.keystore  “XXX”對應於windows作業系統使用者名稱。

主要涉及工具有三個,keytool、jarsigner和zipalign

 1)keytool:生成數字證書,即金鑰,也就是上面說到的副檔名為.keystore的那類檔案;
        2)jarsigner:使用數字證書給apk檔案簽名;
        3)zipalign:對簽名後的apk進行優化,提高與Android系統互動的效率(Android SDK1.6版本開始包含此工具)

通常,可直接通過Ecplise的adt外掛提供的功能來簽名。(詳細可見網路其他資源,本文主要討論shareUserId)

shareUserId介紹:

Android給每個APK程序分配一個單獨的空間,manifest中的userid就是對應一個分配的Linux使用者ID,並且為它建立一個沙箱,以防止影響其他應用程式(或者其他應用程式影響它)。使用者ID 在應用程式安裝到裝置中時被分配,並且在這個裝置中保持它的永久性。

通常,不同的APK會具有不同的userId,因此執行時屬於不同的程序中,而不同程序中的資源是不共享的,在保障了程式執行的穩定。然後在有些時候,我們自己開發了多個APK並且需要他們之間互相共享資源,那麼就需要通過設定shareUserId來實現這一目的。

通過Shared User id,擁有同一個User id的多個APK可以配置成執行在同一個程序中.所以預設就是可以互相訪問任意資料. 也可以配置成執行成不同的程序, 同時可以訪問其他APK的資料目錄下的資料庫和檔案.就像訪問本程式的資料一樣。

shareUserId設定:

在需要共享資源的專案的每個AndroidMainfest.xml中新增shareuserId的標籤。

android:sharedUserId="com.example"

id名自由設定,但必須保證每個專案都使用了相同的sharedUserId。一個mainfest只能有一個Shareuserid標籤。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.shareusertesta"
    android:versionCode="1"
    android:versionName="1.0" 
    android:sharedUserId="com.example">

\data\data\自定義的package\ 路徑下的互相訪問

每個安裝的程式都會根據自己的包名在手機檔案系統的data\data\your package\建立一個資料夾(需要su許可權才能看見),用於儲存程式相關的資料。

在程式碼中,我們通過context操作一些IO資源時,相關檔案都在此路徑的相應資料夾中。比如預設不設定外部路徑的檔案、DB等等。

正常情況下,不同的apk無法互相訪問對應的app資料夾。但通過設定相同的shareUserId後,就可以互相訪問了。程式碼如下。

//程式A:
public class MainActivityA extends Activity {
    TextView textView;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView)findViewById(R.id.textView1);
        WriteSettings(this, "123");
    }


    public void WriteSettings(Context context, String data) {
        FileOutputStream fOut = null;
        OutputStreamWriter osw = null;
        try {
            //預設建立在data/data/xxx/file/ 
            fOut = openFileOutput("settings.dat", MODE_PRIVATE);            
            osw = new OutputStreamWriter(fOut);
            osw.write(data);
            osw.flush();
            Toast.makeText(context, "Settings saved", Toast.LENGTH_SHORT)
                    .show();
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(context, "Settings not saved", Toast.LENGTH_SHORT)
                    .show();
        } finally {
            try {
                osw.close();
                fOut.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
//程式B:
public class MainActivityB extends Activity {
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) this.findViewById(R.id.textView1);
        
        try {
            //獲取程式A的context
            Context ctx = this.createPackageContext(
                    "com.example.shareusertesta",             Context.CONTEXT_IGNORE_SECURITY);
            String msg = ReadSettings(ctxDealFile);
            Toast.makeText(this, "DealFile2 Settings read" + msg,
                    Toast.LENGTH_SHORT).show();
            WriteSettings(ctx, "deal file2 write");
        } catch (NameNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }


    public String ReadSettings(Context context) {
        FileInputStream fIn = null;
        InputStreamReader isr = null;
        char[] inputBuffer = new char[255];
        String data = null;
        try {
            //此處呼叫並沒有區別,但context此時是從程式A裡面獲取的
            fIn = context.openFileInput("settings.dat");
            isr = new InputStreamReader(fIn);
            isr.read(inputBuffer);
            data = new String(inputBuffer);
            textView.setText(data);
            Toast.makeText(context, "Settings read", Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(context, "Settings not read", Toast.LENGTH_SHORT)
                    .show();
        } finally {
            try {
                isr.close();
                fIn.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public void WriteSettings(Context context, String data) {
        FileOutputStream fOut = null;
        OutputStreamWriter osw = null;
        try {
            fOut = context.openFileOutput("settings.dat", MODE_PRIVATE);
            //此處呼叫並沒有區別,但context此時是從程式A裡面獲取的
            osw = new OutputStreamWriter(fOut);
            osw.write(data);
            osw.flush();
            Toast.makeText(context, "Settings saved", Toast.LENGTH_SHORT)
                    .show();

        } catch (Exception e) {
            e.printStackTrace();
            Toast.makeText(context, "Settings not saved", Toast.LENGTH_SHORT)
                    .show();

        } finally {
            try {
                osw.close();
                fOut.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

如果A和B的mainfest中設定了相同的shareuserId,那麼B的read函式就能正確讀取A寫入的內容。否則,B無法獲取該檔案IO。

通過這種方式,兩個程式之間不需要程式碼層級的引用。之間的約束是,B需要知道A的file下面存在“settings.dat”這個檔案以及B需要知道A的package的name。

Resources和SharedPreferences的共享

通過shareuserId共享,我們可獲取到程式A的context。因此,我們就可以通過context來獲取程式A對應的各種資源。比較常用的就是Raw資源的獲取,如一些軟體的apk面板包就是採用了這種技術,將主程式和面板資源包分在兩個apk中。

獲取Resources很簡單,在程式A和B的mainfest中設定好相同的shareuserId後,通過createPackageContext獲取context即可。之後就和原來的方式一樣,通過getResources函式獲取各種資源,只是此時的context環境是目標APP的context環境。

//B中呼叫

Context friendContext = this.createPackageContext( "com.example.shareusertesta",Context.CONTEXT_IGNORE_SECURITY);

//在B中獲取A的各種資源

friendContext.getResources().getString(id);

friendContext.getResources().getDrawable(id);

可看見,與一般獲取資源的方式並沒有區別,只是獲取context時有所不同。很簡單的就能想到我們會在專案中對資源操作、IO操作等分裝一個工具類,通過傳遞context來區分目標,這樣能很好的簡化複雜性。

分析這段程式碼,可看見程式A和B之間的聯絡有三個:

1 mainfest中宣告shareuserId時需要知道一個共同的userId

2 createpackageContext時需要知道目標APK的package的name

3 獲取資源時需要知道該資源的對應ID

資源的R.id的討論

在上面的三個聯絡中,1和2並不複雜,但是“3 獲取資源時需要知道該資源的對應ID”,這一點是一種比較麻煩的約束,會造成一些複雜的情況。

比如,在程式A中我們添加了一個String資源share_test_a ,現在需要在B中獲取該資源。於是我們就通過context.getResources().getString(id)來獲取。

注意,share_test_a是在A中定義的,在A裡面我們可以簡單的通過“R.string.share_test_a”來標示id。但是在程式B中,我們並未在strings.xml中定義過“share_test_a”這個string,因此不存在“R.string.share_test_a”這個標示ID,也就是說連編譯都不通過,。

那麼,我們該怎麼來獲取ID呢?一般會想到兩種方法,一是利用外部儲存檔案儲存A中的這個id,然後在B中讀取id後再獲取資源;二是在B中同樣定義一個”share_test_a”的變數。兩種方案是否可行,我們在下面討論。

SharedPreferences傳遞R.id

先來看下方案一,最簡單的能想到的方式就是File、DB和SharedPreferences。三者原理相同擇一即可。以SharedPreferences舉例。

//程式A中
SharedPreferences sp = this.getSharedPreferences("sp", MODE_PRIVATE);
Editor editor = sp.edit();
editor.putInt("Rkey", R.string.share_test_a);
editor.commit();
//程式B中
Context friendContext = this.createPackageContext("com.example.shareusertesta",Context.CONTEXT_IGNORE_SECURITY);
SharedPreferences sp = friendContext.getSharedPreferences("sp", MODE_PRIVATE);
int Rkey = sp.getInt("Rkey", 0);
String ts = friendContext.getResources().getString(Rkey);

從上面程式碼看到,我們通過SharedPreferences間接的中轉了R的id。兩者的約束是對儲存的內容需要一個key來命名並在兩個app中統一,以及需要知道該key對應的型別(int、string等)。

這種方式可以準確的獲取資源,不需要A和B之間有程式碼級別的引用。但需要新增一層map來協調key的含義。

設定相同的資源名

再看另一種方式,在A和B中都設定相同的資源名。

假如A和B都定義一個“share_test”的變數(即都有R.String.share_test),A的內容是“hello A”,B的內容是”hello B”。有個show的函式。

private String show(Context context){
        return context.getResources().getString(R.string.share_test);
}

看上去似乎很不錯,我們只要傳遞不同的context進去,就能利用R.string.share_test這個共同ID來動態顯示不同的內容,而且不需要調整id的程式碼。

但是,我們再仔細看下關於getString(int id)的引數,它傳遞的是一個int變數值,這個值又從哪裡來呢?在專案的gen資料夾下面能夠找到”R.java”這個檔案,該檔案中對系統的各種變數自動生成了一個唯一標示。

//A的R.java
    public static final class string {
        public static final int action_settings=0x7f050001;
        public static final int app_name=0x7f050000;
        public static final int hello_world=0x7f050002;
        public static final int share_test =0x7f050003;
    }
//B的R.java
public static final class string {
        public static final int action_settings=0x7f050001;
        public static final int app_name=0x7f050000;
        public static final int hello_world=0x7f050002;
        public static final int share_test =0x7f050004;
        public static final int share_test_2=0x7f050003;
    }

仔細看可以發現,R的自動生成規則不是根據你取的名字來固定的。也就是說A和B都定義一個“share_test”,但他們的int值可能不一樣! (事實上,只有極少數情況下才能做到兩個R檔案完全相同。)

因此,通過設定相同的資源名這種方式是不安全的,除非你能確保兩個APP中的R的變數int都相同,否則極容易造成很難發現的隱性bug(邏輯含義錯誤、超界等)。

PS:驗證該問題的時候,我曾異想天開的想到有無可能編譯器或下層程式碼智慧到對R的解析在執行時完成。比如編譯時按照B的Rid通過編譯,但是取值操作是在執行時動態解析完成的,這時候R對應的是A的R,如果有那麼智慧,我們就可以忽略int的不一致了。簡單的測試後證明,這只是我無聊的想法,其實看見形參是int型就基本確認不會存在這種現象了,而且由於context.getResources().getString()是一個通用的函式,如果真這麼做了只會造成數不清的bug。

訪問安全性

上文中通過測試,驗證了同key下設定相同shareuserid後可共享資源,否則失敗。

但還有兩種情況尚未討論。一是假設A和C用兩個不同的簽名,但設定相同的shareuserid,那麼能否共享資源。二是假設A用簽名後的apk安裝,C用usb直連除錯(即debug key),兩者設定相同的shareuserid,那麼能否共享資源。

經過測試,不論是USB除錯還是新簽名APK都安裝不上。

再進一步測試後發現,能否安裝取決於之前手機中是否已經存在對應該shareduserId的應用。如有,則需要判斷簽名key是否相同,如不同則無法安裝。也就是說,如果你刪除a和b的應用先裝c,此時c的安裝正常,而原來a和b的安裝就失敗了(a、b同key,c不同key,三者userId相同)。

其他討論

1 android:sharedUserId="android.uid.system" 如果這麼設定,可實現提權的功能,修改系統時間等需要core許可權的操作就可完成了。但看到有人說會造成sd卡讀取bug,網上有不少解決方案(未測試)。

2 修改shareuserId後,usb開發除錯安裝沒有問題,但是利用Ecplise打包簽名APK後,部分機型會造成無法安裝的問題。網上有提到需要原始碼環境mm打包或其他,較麻煩暫未驗證。

目前測試了三臺機子:三星S3自帶系統失敗;華為一機子成功;三星一刷官方anroid系統的機子成功。初步估計部分廠商修改了一定的核心,造成安裝失敗,具體相容性情況有待進一步測試

3 使用shareuserid後,對同系列的產品的簽名key必須統一,不要丟失。否則後面開發的系列app就無法獲取資料了。此外,注意從沒有userId的版本到有userId版本時的升級,也可能存在一定的安全許可權問題。