Replugin 全面解析(5)

本篇我们来看看四大组件中的BroadcaseReceiverContentProvider。总体来说,这两个组件的生命周期相对简单,所以要在Replugin框架中处理插件的BroadcaseReceiverContentProvider更简单容易一些,框架中的代码逻辑也很好理解。

BroadcaseReceiver

广播我们分两步来讲解:

  • 注册广播
  • 接收广播

Replugin中广播的整体逻辑非常清楚简单,基本上可以用下面这张图来概括。

broadcastreceiver.jpg

注册广播

如果你看过Replugin 全面解析 (3) 你应该会记得在插件加载的过程中,有一个步骤就是解析Plugin中的BroadcastReceiver并注册,我们就从这里开始讲起。当然,在Replugin中通过代码注册广播跟原生并没有什么两样,不同的只是插件中在Manifest文件中静态注册的广播的注册方式有所不同。所以这里我们所讲的就是插件中静态广播的注册流程啦!

Loader.regReceivers先得到插件的名字,调用IPluginHost接口的regReceiver函数,将加载插件Dex时解析出来的广播信息通过远程调用注册到Persistent进程中。

private void regReceivers() throws android.os.RemoteException {
    String plugin = mPluginObj.mInfo.getName();

    if (mPluginHost == null) {
        mPluginHost = getPluginHost();
    }

    if (mPluginHost != null) { // 第一个参数是插件名,第二个参数是广播信息
        mPluginHost.regReceiver(plugin, ManifestParser.INS.getReceiverFilterMap(plugin));
    }
}

远程调用实际是调用Persistent进程中的PmHostSvc.regReceiver函数,这个函数会完成以下任务:

  • 创建PluginReceiverProxy对象,给它添加一个保存Action的Map对象mActionPluginComponents
  • 遍历插件中的广播,并将广播以及广播的IntentFilter信息都保存到mActionPluginComponents
  • PluginReceiverProxy实际上就是一个广播,所以将它注册到Android系统中,并将插件中所有广播的所有IntentFilter都添加到这个广播中。因此当系统发送广播时,所有匹配这些IntentFilter的广播都会首先被PluginReceiverProxy接收到。
public void regReceiver(String plugin, Map rcvFilMap) throws RemoteException {
    ......
    HashMap<String, List<IntentFilter>> receiverFilterMap = (HashMap<String, List<IntentFilter>>) rcvFilMap;
    // 遍历此插件中所有静态声明的 Receiver
    for (HashMap.Entry<String, List<IntentFilter>> entry : receiverFilterMap.entrySet()) {
        if (mReceiverProxy == null) {
            mReceiverProxy = new PluginReceiverProxy();
            mReceiverProxy.setActionPluginMap(mActionPluginComponents);
        }
        String receiver = entry.getKey(); 
        List<IntentFilter> filters = entry.getValue();
        if (filters != null) {
            for (IntentFilter filter : filters) {
                int actionCount = filter.countActions();
                while (actionCount >= 1) {
                    saveAction(filter.getAction(actionCount - 1), plugin, receiver);
                    actionCount--;
                }
                mContext.registerReceiver(mReceiverProxy, filter);//注册PluginReceiverProxy
            }
        }
    }
}

注册广播的过程就这是这么简单!

接收广播

接收广播正如上面所提到的,首先是PluginReceiverProxy.onReceive来处理。

这里会选择使用IPluginHost或者IPluginClient来进一步处理广播。在Replugin 全面解析 (4) 中有讲过这两者的区别。所以如果广播并不是Persistent进程中的,就会使用IPluginClient.onReceive来处理。

public void onReceive(Context context, Intent intent) {
    ......
    String action = intent.getAction();
    if (!TextUtils.isEmpty(action)) {
        ......
        List<String> receivers = new ArrayList<>(entry.getValue());
        for (String receiver : receivers) {
            try {
                ......
                if (process == IPluginManager.PROCESS_PERSIST) {
                    IPluginHost host = PluginProcessMain.getPluginHost();
                    host.onReceive(plugin, receiver, intent); // Persistent进程
                } else {
                    IPluginClient client = MP.startPluginProcess(plugin, process, new PluginBinderInfo(PluginBinderInfo.NONE_REQUEST));
                    client.onReceive(plugin, receiver, intent); // 非Persistent进程
                }
            } catch (Throwable e) {
            }
        }
    }
}


上一步通过远程调用会调用到PluginProcessPer.onReceive,这一步实际上就是调用PluginReceiverHelper.onReceive函数。

  • 查找出插件的Context对象,也就是PluginContext对象
  • 通过PluginDexClassLoader加载BroadcastReceiver类并创建实例对象
  • UI线程调用BroadcastReceiver对象的onReceive函数,这样就完成了广播的处理
public static void onPluginReceiverReceived(final String plugin, final String receiverName, final HashMap<String, BroadcastReceiver> receivers, final Intent intent) {
    ......
    // 使用插件的 Context 对象
    final Context pContext = Factory.queryPluginContext(plugin);
    ......
    String key = String.format("%s-%s", plugin, receiverName);
    BroadcastReceiver receiver = null;
    if (receivers == null || !receivers.containsKey(key)) {
        try {
            // 使用插件的 ClassLoader 加载 BroadcastReceiver
            Class c = loadClassSafety(pContext.getClassLoader(), receiverName);
            if (c != null) {
                receiver = (BroadcastReceiver) c.newInstance();
                if (receivers != null) {
                    receivers.put(key, receiver);
                }
            }
        } catch (Throwable e) {
        }
    } else {
        receiver = receivers.get(key);
    }
    if (receiver != null) {
        final BroadcastReceiver finalReceiver = receiver;
        // 转到 ui 线程
        Tasks.post2UI(new Runnable() {
            @Override
            public void run() {
                finalReceiver.onReceive(pContext, intent);
            }
        });
    }
}

唯一要在最后说明的一点是,由于广播的特殊性,不需要任何额外的设计,就可以完全支持原生广播的所有特性。

  • Host可以向Plugin发送广播,Plugin也可以向Host或者其他Pluglin发送广播。
  • 外部应用可以向Host或者Plugin发送广播,Plugin也可以向外部应用发送广播。
  • 插件内动态注册广播跟原生应用做法也是一样的,并且支持所有原生广播特性。

广播相关的内容就讲解这么多,是不是很简单明了?

ContentProvider

ContentProvider的设计理念跟BroadcastReceiver非常相似,对插件中ContentProvider的访问是通过坑位ContentProvider来代理的,然后坑位ContentProvider 会根据uri去访问插件中的目标ContentProvider以完成数据的CRUD(create, remove, query, delete)操作。先通过一张图来做一个大概了解:

contentprovider.jpg

整个过程分为以下几步:

  • 插件中通过反射调用PluginProviderClient的CRUD操作函数,以uri作为参数
  • 将目标uri组装成一个newUrinewUri指向目标provider所在进程的一个坑位provider
  • 坑位privider接收到操作请求以后从newUri中解析出目标uri
  • 通过ClassLoader加载并创建目标provider的实例,并显示调用对应的CRUD操作

Replugin 全面解析(3) 中,我们在Plugin的环境初始化过程中提到过PluginProviderClient,在replugin-host-lib中也有一个同名的类。在Plugin中需要访问provider的时候,就会调用replugin-pugin-lib中的PluginProviderClient类,它会通过反射调用replugin-host-lib中的PluginProviderClient的方法,我们以insert操作为例。

replugin-pugin-lib中的PluginProviderClient.insert,如果RePluginFramework没有初始化,说明当前的插件是被当作普通应用在使用,所以直接使用原生的provider操作。

public static Uri insert(Context c, Uri uri, ContentValues values) {
    if (!RePluginFramework.mHostInitialized) {
        return c.getContentResolver().insert(uri, values);  // 原生操作
    }

    try { // 反射调用replugin-host-lib中的PluginProviderClient.insert
        return (Uri) ProxyRePluginProviderClientVar.insert.call(null, c, uri, values);
    } catch (Exception e) {
    }
    return null;
}

replugin-host-lib中的PluginProviderClient.insert,这里最重要的一步就是toCalledUri

public static Cursor query(Context c, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
    Uri turi = toCalledUri(c, uri);
    return c.getContentResolver().query(turi, projection, selection, selectionArgs, sortOrder, cancellationSignal);
}

PluginProviderClient.toCalledUri负责将上面的uri参数组装成一个新的newUri,这里要注意:

  • 如果通过ContextUri找不到插件,说明并不是要访问插件中的provider,所以就不需要组装newUri,直接通过系统逻辑访问即可。
  • 如果发现要访问的provider是插件中,则去组装newUri
public static Uri toCalledUri(Context c, Uri uri) {
    String pn = fetchPluginByContext(c, uri);
    if (pn == null) {
        return uri;
    }
    return toCalledUri(c, pn, uri, IPluginManager.PROCESS_AUTO);
}

组装newUri,目的就是将原来的uri于坑位provider的uri组合在一起,成为一个新的uri,但实际上这个uri是用来访问坑位privider的。这里要完成几件事情:

  • 通过Uri中插件名和authority找到想要访问的provider所在的进程
  • 找到对应进程中的坑位providerauthority
  • 将坑位providerauthority与插件providerauthority组合在一起形成newUri,组合规则请看下面代码中的注释部分
public static Uri toCalledUri(Context context, String plugin, Uri uri, int process) {
    ......
    // content://com.qihoo360.mobilesafe.PluginUIP
    if (process == IPluginManager.PROCESS_AUTO) {
        process = getProcessByAuthority(plugin, uri.getAuthority());
        if (process == PROCESS_UNKNOWN) {
            return uri;
        }
    }

    String au;
    if (process == IPluginManager.PROCESS_PERSIST) {
        au = PluginPitProviderPersist.AUTHORITY;
    } else if (PluginProcessHost.isCustomPluginProcess(process)) {
        au = PluginProcessHost.PROCESS_AUTHORITY_MAP.get(process);
    } else {
        au = PluginPitProviderUI.AUTHORITY;
    }
// 插件名plugin_name
// 插件provider的uri => content://com.qihoo360.contacts.abc/people?id=9
// 坑位provider看到uri => content://com.qihoo360.mobilesafe.Plugin.NP.UIP
// 组合后的newUri => content://com.qihoo360.mobilesafe.Plugin.NP.UIP/plugin_name/com.qihoo360.contacts.abc/people?id=9
    String newUri = String.format("content://%s/%s/%s", au, plugin, uri.toString().replace("content://", ""));
    return Uri.parse(newUri);
}

在UI进程,persistent进程以及Replugin默认提供的三个自定义进程中都各自有一个坑位provider,他们的authority是各不相同的,但他们都是PluginPitProviderBase的子类。这几个provider是:

  • PluginPitProviderUI
  • PluginPitProviderPersist
  • PluginPitProviderP0
  • PluginPitProviderP1
  • PluginPitProviderP2

通过newUri就可以访问坑位provider了,insert操作自然是providerinsert函数来处理,而他们都是PluginPitProviderBase的子类,而且没有提供自己的insert函数,所以自然就会使用PluginPitProviderBase提供的insert函数

  • 第一步从参数uri中解析出目标provider的uri,就是从上面的newUri中将插件名后面的部分取出来
  • PluginProviderHelper.getProvider加载目标provider类名并通过反射创建一个实例
  • 使用这个目标provider实例真正执行insert操作,实现最终的数据插入操作

这三个小步骤的代码都很容易理解,有兴趣可以看看源码哦~

public Uri insert(Uri uri, ContentValues values) {
    PluginProviderHelper.PluginUri pu = mHelper.toPluginUri(uri);
    if (pu == null) {
        return null;
    }
    ContentProvider cp = mHelper.getProvider(pu);
    if (cp == null) {
        return null;
    }
    return cp.insert(pu.transferredUri, values);
}

好了,Replugin中ContentProvider的原理就是这样,也很好理解!

注意:

  • 外部应用不能访问插件中的provider
  • 不过插件中是可以访问外部应用的provider
  • Host要访问插件中的provider需要使用PluginProviderClient提供的方法
  • 当然,插件之间是可以相互访问对方的provider

总结

通过这四篇分析,Replugin的大部分核心原理已经都分析过了,当然框架中有很多细节或者小的设计并没有能够全部覆盖到,在需要的时候可以看看代码。在Replugin中还有两个gradle插件,已经有人写过两篇比较详细的分析:

Replugin整体的设计很巧妙,支持的原生特性也很多,当然在目前的阶段坑定也还有一些不能支持的特性,希望在以后的版本中能够逐步得到支持!目前只是在360相关的少数应用上使用,虽然崩溃率很低(万分之三),但还需要接受大面积使用,不同应用场景的应用的检验,在这个过程中发现问题逐步完善!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 由于android系统中应用程序之间不能共享内存。因此,在不同应用程序之间交互数据(跨进程通讯)就稍微麻烦一些。在...
    Ten_Minutes阅读 8,433评论 1 7
  • 前言 replugin-plugin-gradle 是 RePlugin 插件框架中提供给replugin插件用的...
    osan阅读 6,996评论 8 33
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,590评论 18 139
  • 题记 写这篇关于Replugin插件化框架的分析,旨在引导读者去快速的了解RePlugin的大概实现原理,文中会抛...
    Ihesong阅读 1,663评论 0 1
  • 糊涂的人有糊涂的活法 清醒的人有清醒的活法 理想的人追求自由 现实的人追求富裕 而我,却夹杂在理想和现实之间 时而...
    桃夭夭z阅读 291评论 4 5