android IPC 通訊(上)-sharedUserId&&Messenger

Shawn_Dut發表於2019-02-27

  看了一本書,上面有一章講解了IPC(Inter-Process Communication,程式間通訊)通訊,決定結合以前的一篇部落格android 兩個應用之間的通訊與呼叫和自己的理解來好好整理總結一下這塊的知識,由於內容較多,這部分會分上中下三篇部落格來仔細分析講解,第一篇上篇要講解的是sharedUserId和Messenger的使用方式。
  android IPC通訊(中)-ContentProvider&&Socket
  android IPC通訊(下)-AIDL

sharedUserId


  sharedUserId的作用是讓兩個應用程式共享一個user id,我們都知道linux程式給每一個應用程式分配了一個獨立的user id,所以如果兩個或多個應用程式的簽名相同並且設定了一樣的sharedUserId,他們將會共享一個user id,相同user id的應用程式可以訪問對方的資料(也就是說如果應用程式中的一個檔案的許可權是600,相同uid可以直接訪問,反之則無法訪問),並且設定成一個android:process就能夠執行在一個程式中了。
  sharedUserId方式主要就是使用createPackageContext (String packageName, int flags)函式,該函式用來返回指定包名應用的上下文,注意是application的context。
  這個方法有兩個引數:

  1. packageName:包名,要得到Context的應用程式的完整包名

  2. flags:標誌位,有CONTEXT_INCLUDE_CODECONTEXT_IGNORE_SECURITY兩個選項,CONTEXT_INCLUDE_CODE選項的作用就是可以在呼叫者的程式執行該application的程式碼,也就是說可以使用getClassLoader()函式來初始化該application的相關類,使用該標識將會在你可以使用的application context上施加安全約束,如果需要載入的application不能被安全的載入進程式的話,將會丟擲一個SecurityException,如果這個標示沒有被設定,那麼將不會在被載入的類上面施加任何約束,getClassLoader()將會返回預設的系統類載入器;CONTEXT_IGNORE_SECURITY的意思是忽略任何安全警告,和CONTEXT_INCLUDE_CODE標識一起使用可能會將不安全的程式碼載入進程式,所以謹慎使用。


  在我部落格 android permission許可權與安全機制解析(上)中已經簡單介紹了一下SharedUserId,這次就要詳細講解一下他的具體用法,用來A應用和B應用之間的互動。先來看看兩個應用的manifest檔案:
  A應用:
<manifest package="com.android.shareduserid_a"
xmlns:android="schemas.android.com/apk/res/and…"
android:sharedUserId="com.android.test"
android:sharedUserLabel="@string/share_label">


<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity android:name="com.android.shareuserid_a.Server">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>

</application>

</manifest>
複製程式碼
B應用:
<manifest package="com.android.shareduserid_b"
xmlns:android="schemas.android.com/apk/res/and…"
android:sharedUserId="com.android.test"
android:sharedUserLabel="@string/share_label">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity android:name="com.android.shareuserid_b.Client"
android:exported="true">

</activity>
</application>
</manifest>
複製程式碼


  B應用的Client Activity加上了android:exported=”true”用來與A應用互動,這個在 android permission許可權與安全機制解析(上)已經有詳細的介紹,在這就略過了。
  A應用有一個Server的Activity,B應用有一個Client的Activity,先來看看互動的效果,左側為A應用,右側為B應用:
  這裡寫圖片描述
  A應用中的Server activity通過下面程式碼獲取到B應用的application context:

context = createPackageContext("com.android.shareduserid_b", CONTEXT_INCLUDE_CODE|CONTEXT_IGNORE_SECURITY);複製程式碼

  獲取到該application的上下文之後,我們能做的事情就很多了,下面介紹幾種:

獲取B應用drawable



  獲取B應用的drawable,使用getIdentifier函式即可獲取B應用該資源的id:
//獲取B應用圖片
int id = context.getResources().getIdentifier("share", "mipmap", "com.android.shareduserid_b");
iv_pic.setImageDrawable(ContextCompat.getDrawable(context, id));複製程式碼




獲取B應用string



  獲取B應用字串,統一使用getIdentifier函式獲取id:
id = context.getResources().getIdentifier("share_string", "string", "com.android.shareduserid_b");
tv_string.setText(context.getString(id));複製程式碼

開啟B應用頁面


  開啟B應用activity,使用ComponentName設定B應用的activity名稱和包名就能成功跳轉過去了,類名需要帶上完整的包名,當然使用intent.setClassName()函式也是能夠成功的,記得在B應用一定要加上android:exported=”true”:
Intent intent = new Intent();
ComponentName componentName = new ComponentName("com.android.shareduserid_b",
"com.android.shareuserid_b.Client");
intent.setComponent(componentName);
//intent.setClassName(context, "com.android.shareuserid_b.Client");
//intent.setClassName("com.android.shareduserid_b",
// "com.android.shareuserid_b.Client");
startActivity(intent);複製程式碼

執行B應用指定類函式


  執行B應用函式,當然是利用反射,要執行B應用的程式碼,呼叫createPackageContext函式時flag中一定要有CONTEXT_INCLUDE_CODE。如果被反射的函式引數是可變型別,類似於int… integers,那麼getMethod函式第二個引數傳入一個int[]陣列就可以了。注意在B應用中要有相關的類和函式:
try {
Class clazz = context.getClassLoader().loadClass("com.android.shareuserid_b.Method");
Object object = clazz.newInstance();
int[] ints = new int[]{1,2,3};
int sum = (int) clazz.getMethod("add", int[].class).invoke(object, ints);
tv_sum.setText("sum is :"+sum);
} catch (Exception e) {
L.e(e);
e.printStackTrace();
}複製程式碼

獲取B應用SharedPreferences


  獲取B應用SharedPreferences,這個需要特殊說明一下,由於SharedPreferences是有快取機制的,所以如果在B應用中修改了該SharedPreferences檔案,接著A應用去讀取該檔案中修改的那個值,這時你會發現還是修改前的值,這就是快取機制導致的問題,不過有一個flag可以解決這個問題:MODE_MULTI_PROCESS,但是非常不幸的是api23已經將該標識deprecated了,原因是在一些版本上不可靠,有興趣的可以去了解一下,看程式碼:


//注意Context.MODE_MULTI_PROCESS不可靠
SharedPreferences sharedPreferences = context.getSharedPreferences("permanent", MODE_MULTI_PROCESS);
String time = sharedPreferences.getString("time", "get time error");
tv_shared_preference.setText(time);複製程式碼

獲取B應用資料庫


  獲取B應用的資料庫,注意資料庫名字和表名一定要對應上,要不然會丟擲Exception:
String DBPath = context.getDatabasePath("permanentCache.db").getAbsolutePath();
SQLiteDatabase sqLiteDatabase = SQLiteDatabase.openDatabase(DBPath, null, SQLiteDatabase.OPEN_READONLY);
Cursor cursor = sqLiteDatabase.query("cache_1", null, "key=?", new String[]{"time"}, null, null, null, null);
cursor.moveToNext();
tv_DB.setText(cursor.getString(1));
cursor.close();
sqLiteDatabase.close();複製程式碼


用途:這種方式可以用來進行輕量級的補丁操作,例如皮膚,第一步從伺服器獲取所有皮膚的包名,第二步看使用者選擇的皮膚包是否已經安裝到手機上,如果沒有從伺服器下載安裝,如果有直接第三步;第三步當然就是從該皮膚包中獲取資源等等等了。

下載地址

原始碼地址:github.com/zhaozepeng/…

Messenger

  Messenger可以用來在不同程式中傳遞物件,在Messenger中放入我們需要傳遞的物件,就能輕鬆地實現資料的程式間傳遞了。Messenger是一種輕量級的IPC方案,它對AIDL進行封裝,所以使用起來非常的方便,當然AIDL通訊的底層實現也是對Binder的封裝,需要特別注意的是這個Binder類並不會影響系統對程式生命週期的管理(你需要使用一些更高等級的元件來告訴系統你的程式需要繼續執行而不被系統回收記憶體),如果由於某些原因被系統殺死回收,連線就會斷開。

  因為是程式之間的通訊,所以可以在一個應用中開兩個程式通訊,也可以在兩個應用中實現通訊,我就以兩個應用之間的通訊為例。兩個應用之間的通訊可以使用兩種方式:

  1. 客戶端使用bindService函式繫結服務端Service,並且利用handler進行兩個應用之間message的處理。
  2. 客戶端使用bindService繫結服務端Service,然後直接使用反射機制,讓客戶端反射service端的函式來進行操作,注意,這種方式一定要讓兩個應用執行在同一個程式中(使用sharedUserId+android:process),要不然無法反射出相關函式,具體程式碼中會指出。

Handler處理方式

  handler方式最主要是通過Messenger+message進行兩個應用的通訊,先來看看服務端Service程式碼:

Messenger messenger = null;
private static class MessengerHandler extends Handler{
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what){
            case 1:
                L.i("i receive '" + msg.getData().getString("message")+"'");
                Messenger client = msg.replyTo;

                //迴應客戶端
                if (client != null){
                    Message reply = Message.obtain();
                    Bundle message = new Bundle();
                    message.putString("message", "i have received your message");
                    L.i("i have received your message");
                    reply.setData(message);
                    try {
                        client.send(reply);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case 2:
                L.i("i receive '" + msg.getData().getString("message")+"'");
                L.i("client has disconnect this connection, bye~");
                break;
            default:
                break;
        }
    }
}

@Override
public IBinder onBind(Intent intent) {
    return messenger.getBinder();
}

@Override
public void onCreate() {
    super.onCreate();
    messenger = new Messenger(new MessengerHandler());
}複製程式碼

  客戶端程式碼:

private Button connect_handler;
private TextView tv_handler;
private Button connect_binder;
private TextView tv_binder;

private ServiceConnection serviceConnection;
private Messenger serverMessenger;
private Messenger messenger;

private boolean hasBindService = false;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.client_handler);
    connect_handler = (Button) findViewById(R.id.connect_handler);
    connect_handler.setOnClickListener(this);
    tv_handler = (TextView) findViewById(R.id.tv_handler);
    connect_binder = (Button) findViewById(R.id.connect_binder);
    connect_binder.setOnClickListener(this);
    tv_binder = (TextView) findViewById(R.id.tv_binder);

    serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            serverMessenger = new Messenger(service);
            communicate();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            serverMessenger = null;
        }
    };

    messenger = new Messenger(new Handler(){
        @Override
        public void handleMessage(Message msg) {
            L.i("i have received '" + msg.getData().getString("message") + "'");
            Message message = Message.obtain();
            Bundle bundle = new Bundle();
            bundle.putString("message", "OK, bye bye~");
            message.setData(bundle);
            L.i("i have send '" + message.getData().getString("message") + "'");
            message.what = 2;
            if (serverMessenger != null){
                try {
                    serverMessenger.send(message);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    });
}

private void communicate(){
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
    Message message = Message.obtain();
    Bundle msg = new Bundle();
    msg.putString("message", "i have send handler a message at " + simpleDateFormat.format(System.currentTimeMillis()));
    message.setData(msg);
    L.i("i have send '" + message.getData().getString("message") + "'");
    message.what = 1;
    message.replyTo = messenger;
    if (serverMessenger != null){
        try {
            serverMessenger.send(message);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

@Override
public void onClick(View v) {
    switch (v.getId()){
        case R.id.connect_handler:
            if (!hasBindService) {
                Intent intent = new Intent();
                intent.setClassName("com.android.messenger_a", "com.android.messenger_a.ServerWithHandler");
                bindService(intent, serviceConnection, BIND_AUTO_CREATE);
                hasBindService = true;
            }else{
                if (serverMessenger == null){
                    return;
                }
                communicate();
            }
            break;
        case R.id.connect_binder:
            startActivity(new Intent(this, ClientForBinder.class));
            break;
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    if (serverMessenger != null)
        unbindService(serviceConnection);
}複製程式碼

  執行結果如下圖:

  這裡寫圖片描述

  伺服器端Service先需要自定義一個Handler類用來處理客戶端發過來的訊息,接著用一個Handler物件新建一個Messenger物件,這樣就相當於把傳送給Messenger的message處理交給了該Handler,最後就在onBind函式中返回該Messenger的IBinder即可。

  客戶端用bindService繫結該服務端service,並且在onServiceConnected回撥中用IBinder引數構造一個Messenger,這個Messenger就是客戶端用來和服務端通訊的中介了,還有一點需要注意的是,為了服務端接收到客戶端的訊息之後能夠回覆客戶端,在客戶端也需要新建一個Messenger,並且將其通過message.replyTo變數傳遞給服務端,服務端就能夠通過該replyTo變數傳遞訊息給客戶端了。

反射方式

  該模式需要使用sharedUserId+android:permission的方式將兩個應用置於一個程式才能使用(這樣想一想好像就不是跨程式通訊了呢-, -),要不然是無法反射到相關函式的。

  服務端程式碼:

private final InnerBinder binder = new InnerBinder();

public class InnerBinder extends Binder {
    public ServerWithBinder getServer(){
        return ServerWithBinder.this;
    }
}

public int add(int... ints){
    int sum = 0;
    for (int temp : ints){
        sum += temp;
    }
    return sum;
}

@Override
public IBinder onBind(Intent intent) {
    return binder;
}複製程式碼

  客戶端程式碼:

private ServiceConnection serviceConnection;
private Messenger serverMessenger;
private IBinder mBoundService;
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.client_binder);
    findViewById(R.id.connect_binder).setOnClickListener(this);

    serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            try {
                mBoundService = service;
                Class clazz = mBoundService.getClass();
                //如果兩個應用程式沒有執行在同一個程式中,則無法反射到該函式
                Method method = clazz.getDeclaredMethod("getServer");
                Object object = method.invoke(mBoundService);
                Class messenger = object.getClass();
                Method add = messenger.getDeclaredMethod("add", int[].class);
                L.e("1+2+3=" + add.invoke(object, new int[]{1,2,3}));
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            serverMessenger = null;
        }
    };
}

@Override
public void onClick(View v) {
    Intent intent = new Intent();
    intent.setClassName("com.android.messenger_a", "com.android.messenger_a.ServerWithBinder");
    bindService(intent, serviceConnection, BIND_AUTO_CREATE);
}

@Override
protected void onDestroy() {
    if (serverMessenger != null)
        unbindService(serviceConnection);
    super.onDestroy();
}複製程式碼

  執行結果如下圖所示:

  這裡寫圖片描述

  客戶端通過onServiceConnected函式回撥獲取到該Binder物件,通過該Binder物件反射服務端Binder類的相關方法,在服務端的該相關方法getServer()中,直接返回該Service物件,獲取到該Service物件之後就能夠成功反射該類的所有方法,也就能夠成功進行兩個應用之間(雖然說不是兩個程式)的通訊了。

下載地址

原始碼地址:github.com/zhaozepeng/…

Service相關flag介紹

  關於Service的詳細介紹可以看我以前的一篇部落格:

  blog.csdn.net/self_study/…

  這裡簡單介紹Service的相關flag標識,每次通過startService(Intent)函式啟動Service都會呼叫到onStartCommand (Intent intent, int flags, int startId)函式,該函式第二個引數可以為 0,START_FLAG_REDELIVERY或者START_FLAG_RETRY

  1. START_FLAG_REDELIVERY
  2. 讓系統重新傳送一個intent,這樣如果你的服務在處理它的時候被Kill掉,Intent不會丟失。
  3. START_FLAG_RETRY
  4. 表示服務之前被設為START_STICKY,則會被傳入這個標記。
  onStartCommand (Intent intent, int flags, int startId)函式返回值有四個START_STICKYSTART_NOT_STICKYSTART_REDELIVER_INTENTSTART_STICKY_COMPATIBILITY
  1. START_STICKY
  2. 如果service程式被kill掉,保留service的狀態為開始狀態,但不保留遞送的intent物件。隨後系統會嘗試重新建立service,由於服務狀態為開始狀態,所以建立服務後一定會呼叫onStartCommand(Intent,int,int)方法。如果在此期間沒有任何啟動命令被傳遞到service,那麼引數Intent將為null。
  3. START_NOT_STICKY
  4. “非粘性的”,使用這個返回值時,如果在執行完onStartCommand後,服務被異常kill掉,系統不會自動重啟該服務
  5. START_REDELIVER_INTENT
  6. 重傳Intent。使用這個返回值時,如果在執行完onStartCommand後,服務被異常kill掉,系統會自動重啟該服務,並將Intent的值傳入。
  7. START_STICKY_COMPATIBILITY
  8. START_STICKY的相容版本,但不保證服務被kill後一定能重啟。

相關文章