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版本時的升級,也可能存在一定的安全權限問題。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM