1. 程式人生 > >外掛化開發---DroidPlugin對廣播的管理

外掛化開發---DroidPlugin對廣播的管理

回想一下我們日常開發的時候是如何使用BroadcastReceiver的:註冊, 傳送和接收;因此,要實現BroadcastReceiver的外掛化就這三種操作提供支援;接下來我們將一步步完成這個過程。
我們可以註冊一個BroadcastReceiver然後接收我們感興趣的廣播,也可以給某有緣人發出某個廣播;因此,我們對原始碼的分析按照兩條路線展開:
註冊過程
不論是靜態廣播還是動態廣播,在使用之前都是需要註冊的;動態廣播的註冊需要藉助Context類的registerReceiver方法,而靜態廣播的註冊直接在AndroidManifest.xml中宣告即可;我們首先分析一下動態廣播的註冊過程。
Context類的registerReceiver的真正實現在ContextImpl裡面,而這個方法間接呼叫了registerReceiverInternal,原始碼如下:

    @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
            String broadcastPermission, Handler scheduler) {
        return registerReceiverInternal(receiver, getUserId(),
                filter, broadcastPermission, scheduler, getOuterContext());
    }

    private
Intent registerReceiverInternal(BroadcastReceiver receiver, int userId, IntentFilter filter, String broadcastPermission, Handler scheduler, Context context) { IIntentReceiver rd = null; if (receiver != null) { if (mPackageInfo != null && context != null
) { if (scheduler == null) { scheduler = mMainThread.getHandler(); } rd = mPackageInfo.getReceiverDispatcher( receiver, context, scheduler, mMainThread.getInstrumentation(), true); } else { if (scheduler == null) { scheduler = mMainThread.getHandler(); } rd = new LoadedApk.ReceiverDispatcher( receiver, context, scheduler, null, true).getIIntentReceiver(); } } try { return ActivityManagerNative.getDefault().registerReceiver( mMainThread.getApplicationThread(), mBasePackageName, rd, filter, broadcastPermission, userId); } catch (RemoteException e) { return null; } }

可以看到,BroadcastReceiver的註冊也是通過AMS完成的;在進入AMS跟蹤它的registerReceiver方法之前,我們先弄清楚這個IIntentReceiver型別的變數rd是什麼。

public interface IIntentReceiver extends IInterface

public interface IInterface
{
    /**
     * Retrieve the Binder object associated with this interface.
     * You must use this instead of a plain cast, so that proxy objects
     * can return the correct result.
     */
    public IBinder asBinder();
}

這個類是通過AIDL工具生成的,它是一個Binder物件,因此可以用來跨程序傳輸;
由於廣播的分發過程是在AMS中進行的,而AMS所在的程序和BroadcastReceiver所在的程序不一樣,因此要把廣播分發到BroadcastReceiver具體的程序需要進行跨程序通訊,這個通訊的載體就是IIntentReceiver類。另外,IIntentReceiver是一個介面,從上述程式碼中可以看出,它的實現類為LoadedApk.ReceiverDispatcher。

AMS類的registerReceiver方法程式碼有點多,這裡不一一解釋了,感興趣的話可以自行查閱;這個方法主要做了以下兩件事:

    1、對傳送者的身份和許可權做出一定的校檢
    2、把這個BroadcastReceiver以BroadcastFilter的形式儲存在AMS的mReceiverResolver變數中,供後續使用。

就這樣,被傳遞過來的BroadcastReceiver已經成功地註冊在系統之中,能夠接收特定型別的廣播了;

那麼註冊在AndroidManifest.xml中的靜態廣播是如何被系統感知的呢???
在 外掛載入機制 中我們知道系統會通過PackageParser解析Apk中的AndroidManifest.xml檔案,因此我們有理由認為,系統會在解析AndroidMafest.xml的標籤(也即靜態註冊的廣播)的時候儲存相應的資訊;而Apk的解析過程是在PMS中進行的,因此靜態註冊廣播的資訊儲存在PMS中。

傳送和接收過程

傳送過程
就是一句context.sendBroadcast(),我們順藤摸瓜,跟蹤這個方法。前文也提到過,Context中方法的呼叫都會委託到ContextImpl這個類,我們直接看ContextImpl對這個方法的實現:

 @Override
    public void sendBroadcast(Intent intent) {
        warnIfCallingFromSystemProcess();
        String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
        try {
            intent.prepareToLeaveProcess();
            ActivityManagerNative.getDefault().broadcastIntent(
                mMainThread.getApplicationThread(), intent, resolvedType, null,
                Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE, false, false,
                getUserId());
        } catch (RemoteException e) {
        }
    }

嗯,傳送廣播也是通過AMS進行的,我們直接檢視ActivityManagerService類的broadcastIntent方法,這個方法僅僅是呼叫了broadcastIntentLocked方法,
某個廣播被髮送之後,AMS會找出所有註冊過的BroadcastReceiver中與這個廣播匹配的接收者,然後將這個廣播分發給相應的接收者處理。

匹配過程
某一條廣播被髮出之後,並不是阿貓阿狗都能接收它並處理的;BroadcastReceiver可能只對某些型別的廣播感興趣,因此它也只能接收和處理這種特定型別的廣播;在broadcastIntentLocked方法內部有如下程式碼:

receivers = collectReceiverComponents(intent, resolvedType, callingUid, users);
registeredReceivers = mReceiverResolver.queryIntent(intent, resolvedType, false, userId);

讀者可以自行跟蹤這兩個方法的程式碼,過程比較簡單,我這裡直接給出結論:

receivers是對這個廣播感興趣的靜態BroadcastReceiver列表;collectReceiverComponents 通過PackageManager獲取了與這個廣播匹配的靜態BroadcastReceiver資訊;這裡也證實了我們在分析BroadcasrReceiver註冊過程中的推論——靜態BroadcastReceiver的註冊過程的確實在PMS中進行的。
mReceiverResolver儲存了動態註冊的BroadcastReceiver的資訊;還記得這個mReceiverResolver嗎?我們在分析動態廣播的註冊過程中發現,動態註冊的BroadcastReceiver的相關資訊最終儲存在此物件之中;在這裡,通過mReceiverResolver物件匹配出了對應的BroadcastReceiver供進一步使用。

現在系統通過PMS拿到了所有符合要求的靜態BroadcastReceiver,然後從AMS中獲取了符合要求的動態BroadcastReceiver;因此接下來的工作非常簡單:喚醒這些廣播接受者。簡單來說就是回撥它們的onReceive方法。

接收過程
通過上文的分析過程我們知道,在AMS的broadcastIntentLocked方法中找出了符合要求的所有BroadcastReceiver;接下來就需要把這個廣播分發到這些接收者之中。在broadcastIntentLocked方法的後半部分有如下程式碼:

BroadcastQueue queue = broadcastQueueForIntent(intent);
BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp,
        callerPackage, callingPid, callingUid, resolvedType,
        requiredPermissions, appOp, brOptions, receivers, resultTo, resultCode,
        resultData, resultExtras, ordered, sticky, false, userId);

boolean replaced = replacePending && queue.replaceOrderedBroadcastLocked(r);
if (!replaced) {
    queue.enqueueOrderedBroadcastLocked(r);
    queue.scheduleBroadcastsLocked();
}

首先建立了一個BroadcastRecord代表此次傳送的這條廣播,然後把它丟進一個佇列,最後通過scheduleBroadcastsLocked通知佇列對廣播進行處理。

在BroadcastQueue中通過Handle排程了對於廣播處理的訊息,排程過程由processNextBroadcast方法完成,而這個方法通過performReceiveLocked最終呼叫了IIntentReceiver的performReceive方法。

這個IIntentReceiver正是在廣播註冊過程中由App程序提供給AMS程序的Binder物件,現在AMS通過這個Binder物件進行IPC呼叫通知廣播接受者所在程序完成餘下操作。在上文我們分析廣播的註冊過程中提到過,這個IItentReceiver的實現是LoadedApk.ReceiverDispatcher;我們檢視這個物件的performReceive方法,原始碼如下:

public void performReceive(Intent intent, int resultCode, String data,
        Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
    Args args = new Args(intent, resultCode, data, extras, ordered,
            sticky, sendingUser);
    if (!mActivityThread.post(args)) {
        if (mRegistered && ordered) {
            IActivityManager mgr = ActivityManagerNative.getDefault();
            args.sendFinished(mgr);
        }
    }
}

這個方法建立了一個Args物件,然後把它post到了mActivityThread這個Handler中;我們檢視Args類的run方法:
我們看到了相應BroadcastReceiver的onReceive回撥;因此,廣播的工作原理到這裡就水落石出了;

BroadcastReceiver有一個IntentFilter的概念,也就是說,每一個BroadcastReceiver只對特定的Broadcast感興趣;而且,AMS在進行廣播分發的時候,也會對這些BroadcastReceiver與發出的廣播進行匹配,只有Intent匹配的Receiver才能收到廣播;在分析原始碼的時候也提到了這個匹配過程。如果我們嘗試用替身Receiver解決靜態註冊的問題,那麼它的IntentFilter該寫什麼?我們無法預料外掛中靜態註冊的Receiver會使用什麼型別的IntentFilter,就算我們在AndroidManifest.xml中宣告替身也沒有用——我們壓根兒收不到與我們的IntentFilter不匹配的廣播。其實,我們對於Activity的處理方式也有這個問題;如果你嘗試用IntentFilter的方式啟動Activity,這並不能成功;

可以把靜態廣播當作動態廣播處理

既然都是廣播,它們的功能都是訂閱一個特定的訊息然後執行某個特定的操作,我們完全可以把外掛中的靜態廣播全部註冊為動態廣播,這樣就解決了靜態廣播的問題。

靜態廣播非靜態的實現
要把外掛中的靜態BroadcastReceiver當作動態BroadcastReceiver處理,我們首先得知道外掛中到底註冊了哪些廣播;這個過程歸根結底就是獲取AndroidManifest.xml中的標籤下面的內容,我們可以選擇手動解析xml檔案;這裡我們選擇使用系統的 PackageParser 幫助解析,這種方式在之前的 [外掛載入過程][] 中也用到過,如果忘記了可以溫習一下,我們要解析apk的的資訊,可以使用PackageParser的generateActivityInfo方法。

知道這一點之後,程式碼就比較簡單了;使用反射呼叫相應的隱藏介面,並且在必要的時候構造相應引數的方式我們在外掛化系列文章中已經講述過很多,相信讀者已經熟練,這裡就不贅述,直接貼程式碼:

註冊
我們已經解析得到了外掛中靜態註冊的BroadcastReceiver的資訊,現在我們只需要把這些靜態廣播動態註冊一遍就可以了;但是,由於BroadcastReceiver的實現類存在於外掛之後,我們需要手動用ClassLoader來載入它;這一點在 外掛載入機制 已有講述,不囉嗦了。

、、、、、、、、、、、、、、、、、、、、程式碼區、、、、、、、、、、、、、、、、、、、、、、、
首先是註冊2個靜態廣播的apk
MainActivity什麼都沒有做,只有一個空的佈局檔案

package com.wang.myapplication;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;

public class MainActivity extends Activity  {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }
    // 這個方法比onCreate呼叫早; 在這裡Hook比較好.
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
    }
}

Receiver1

package com.wang.myapplication;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;

public class Receiver1 extends BroadcastReceiver {
    static final String ACTION = "com.weishu.upf.demo.app2.PLUGIN_ACTION";

    public void onReceive(Context paramContext, Intent paramIntent) {
        Toast.makeText(paramContext, "我是外掛, 主程式收到請回答!", 0).show();
        paramContext.sendBroadcast(new Intent("com.weishu.upf.demo.app2.PLUGIN_ACTION"));
    }
}

配置檔案如下

 <receiver android:name="com.wang.myapplication.Receiver1">
            <intent-filter>
                <action android:name="com.wang.myapplication.Receiver1"></action>
            </intent-filter>
        </receiver>
        <receiver android:name="com.wang.myapplication.Receiver2"></receiver>

然後將其打包成apk放到到另一個專案的資原始檔下,我們繼續看另外一個專案的結構
這裡寫圖片描述

MainActivity

package com.weishu.upf.receiver_management.app;

import java.io.File;

import android.app.Activity;
import android.app.ActivityManagerNative;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {

    // 傳送廣播到外掛之後, 外掛如果受到, 那麼會回傳一個ACTION 為這個值的廣播;
    static final String ACTION = "com.weishu.upf.demo.app2.PLUGIN_ACTION";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Button t = new Button(this);
        setContentView(t);
        t.setText("send broadcast to plugin: demo");
        t.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getApplicationContext(), "外掛外掛!收到請回答!!", Toast.LENGTH_SHORT).show();
                sendBroadcast(new Intent("com.wang.myapplication.Receiver1"));
            }
        });

        Utils.extractAssets(this, "test.apk");
        File testPlugin = getFileStreamPath("test.apk");
        try {
            ReceiverHelper.preLoadReceiver(this, testPlugin);
            Log.i(getClass().getSimpleName(), "hook success");
        } catch (Exception e) {
            throw new RuntimeException("receiver load failed", e);
        }

        // 註冊外掛收到我們傳送的廣播之後, 回傳的廣播
        registerReceiver(mReceiver, new IntentFilter(ACTION));
    }

    BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, "外掛外掛,我是主程式,握手完成!", Toast.LENGTH_SHORT).show();
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();

        unregisterReceiver(mReceiver);
    }
}

ReceiverHelper

package com.weishu.upf.receiver_management.app;

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.util.Log;

public final class ReceiverHelper {

    private static final String TAG = "ReceiverHelper";

    public static Map<ActivityInfo, List<? extends IntentFilter>> sCache =
            new HashMap<ActivityInfo, List<? extends IntentFilter>>();

    /**
     * 解析Apk檔案中的 <receiver>, 並存儲起來
     *
     * @param apkFile
     * @throws Exception
     */
    private static void parserReceivers(File apkFile) throws Exception {
        Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
        Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

        Object packageParser = packageParserClass.newInstance();

        // 首先呼叫parsePackage獲取到apk物件對應的Package物件
        Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, PackageManager.GET_RECEIVERS);

        // 讀取Package物件裡面的receivers欄位,注意這是一個 List<Activity> (沒錯,底層把<receiver>當作<activity>處理)
        // 接下來要做的就是根據這個List<Activity> 獲取到Receiver對應的 ActivityInfo (依然是把receiver資訊用activity處理了)
        Field receiversField = packageObj.getClass().getDeclaredField("receivers");
        List receivers = (List) receiversField.get(packageObj);

        // 呼叫generateActivityInfo 方法, 把PackageParser.Activity 轉換成
        Class<?> packageParser$ActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
        Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Class<?> userHandler = Class.forName("android.os.UserHandle");
        Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
        int userId = (Integer) getCallingUserIdMethod.invoke(null);
        Object defaultUserState = packageUserStateClass.newInstance();

        Class<?> componentClass = Class.forName("android.content.pm.PackageParser$Component");
        Field intentsField = componentClass.getDeclaredField("intents");

        // 需要呼叫 android.content.pm.PackageParser#generateActivityInfo(android.content.pm.ActivityInfo, int, android.content.pm.PackageUserState, int)
        Method generateReceiverInfo = packageParserClass.getDeclaredMethod("generateActivityInfo",
                packageParser$ActivityClass, int.class, packageUserStateClass, int.class);

        // 解析出 receiver以及對應的 intentFilter
        for (Object receiver : receivers) {
            ActivityInfo info = (ActivityInfo) generateReceiverInfo.invoke(packageParser, receiver, 0, defaultUserState, userId);
            List<? extends IntentFilter> filters = (List<? extends IntentFilter>) intentsField.get(receiver);
            sCache.put(info, filters);
        }

    }

    public static void preLoadReceiver(Context context, File apk) throws Exception {
        parserReceivers(apk);

        ClassLoader cl = null;
        for (ActivityInfo activityInfo : ReceiverHelper.sCache.keySet()) {
            Log.i(TAG, "preload receiver:" + activityInfo.name);
            List<? extends IntentFilter> intentFilters = ReceiverHelper.sCache.get(activityInfo);
            if (cl == null) {
                cl = CustomClassLoader.getPluginClassLoader(apk, activityInfo.packageName);
            }

            // 把解析出來的每一個靜態Receiver都註冊為動態的
            for (IntentFilter intentFilter : intentFilters) {
                BroadcastReceiver receiver = (BroadcastReceiver) cl.loadClass(activityInfo.name).newInstance();
                context.registerReceiver(receiver, intentFilter);
            }
        }
    }
}

CustomClassLoader

package com.weishu.upf.receiver_management.app;

import java.io.File;
import java.io.IOException;

import dalvik.system.DexClassLoader;

public class CustomClassLoader extends DexClassLoader {
    public CustomClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, libraryPath, parent);
    }

    /**
     * 便利方法: 獲取外掛的ClassLoader, 能夠載入指定的外掛中的類
     *
     * @param plugin
     * @param packageName
     * @return
     * @throws IOException
     */
    public static CustomClassLoader getPluginClassLoader(File plugin, String packageName) throws IOException {
        return new CustomClassLoader(plugin.getPath(),
                Utils.getPluginOptDexDir(packageName).getPath(),
                Utils.getPluginLibDir(packageName).getPath(),
                UPFApplication.getContext().getClassLoader());
    }

}

Utils

package com.weishu.upf.receiver_management.app;

import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

import android.content.Context;
import android.content.res.AssetManager;
public class Utils {

    /**
     * 把Assets裡面得檔案複製到 /data/data/files 目錄下
     *
     * @param context
     * @param sourceName
     */
    public static void extractAssets(Context context, String sourceName) {
        AssetManager am = context.getAssets();
        InputStream is = null;
        FileOutputStream fos = null;
        try {
            is = am.open(sourceName);
            File extractFile = context.getFileStreamPath(sourceName);
            fos = new FileOutputStream(extractFile);
            byte[] buffer = new byte[1024];
            int count = 0;
            while ((count = is.read(buffer)) > 0) {
                fos.write(buffer, 0, count);
            }
            fos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            closeSilently(is);
            closeSilently(fos);
        }

    }

    /**
     * 待載入外掛經過opt優化之後存放odex得路徑
     */
    public static File getPluginOptDexDir(String packageName) {
        return enforceDirExists(new File(getPluginBaseDir(packageName), "odex"));
    }

    /**
     * 外掛得lib庫路徑, 這個demo裡面沒有用
     */
    public static File getPluginLibDir(String packageName) {
        return enforceDirExists(new File(getPluginBaseDir(packageName), "lib"));
    }

    // --------------------------------------------------------------------------
    private static void closeSilently(Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (Throwable e) {
            // ignore
        }
    }

    private static File sBaseDir;

    // 需要載入得外掛得基本目錄 /data/data/<package>/files/plugin/
    private static File getPluginBaseDir(String packageName) {
        if (sBaseDir == null) {
            sBaseDir = UPFApplication.getContext().getFileStreamPath("plugin");
            enforceDirExists(sBaseDir);
        }
        return enforceDirExists(new File(sBaseDir, packageName));
    }

    private static synchronized File enforceDirExists(File sBaseDir) {
        if (!sBaseDir.exists()) {
            boolean ret = sBaseDir.mkdir();
            if (!ret) {
                throw new RuntimeException("create dir " + sBaseDir + "failed");
            }
        }
        return sBaseDir;
    }
}

UPFApplication

package com.weishu.upf.receiver_management.app;

import android.app.Application;
import android.content.Context;

public class UPFApplication extends Application {

    private static Context sContext;

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        sContext = base;
    }

    public static Context getContext() {
        return sContext;
    }
}

最後看這個專案在data/data/目錄下的結構
這裡寫圖片描述