Hermes跨进程通讯代码封装(仿写)

前言

看了lance老师的视频教程后,自己模仿写的Hermes的封装,代码上可能跟老师的有点不太一样,但是思路是一致的。

效果

ezgif-1-2d5f50fed0.gif

核心思想

  1. aidl跨进程通讯
    android中跨进程通讯,自然而然的肯定是使用了aidl,但是aidl使用起来,相对会比较麻烦,因为每次都要自己写一个aidl文件,然后创建service,在service中返回binder进行通讯。

  2. 动态代理
    在c端可以提供一个接口,然后创建动态代理,每次调用方法时,获取到方法名,然后通过aidl跨进程调用,在s端,再通过方法名和classid反射调用对应的方法。

  3. 注释
    视频中,老师通过注释,为了来让给c端和s端操作的类保持一致,这里我也采用相同方式,当然也可以简单操作,直接使用类名,我认为也不是什么问题。

     注意:这里操作的跨进程对象是单例,和视频中保持一致
    

代码解析

1.主进程注册类

class MainActivity : AppCompatActivity() {
    ...
    override fun onCreate(savedInstanceState: Bundle?) {
        ...
       Hermes.register(UserManager::class.java)  
   }
}

class Hermes{
      ...
      fun register(cls:Class<*>){
            //保存2个map, clsid-cls,cls-method
            cls.declaredAnnotations.forEach { annotation ->
                if (annotation is ClassId){
                    clsIdMap.put(annotation.id,cls)
                    clsMethodMap.put(cls,cls.declaredMethods)
                }
            }
        }
      ...
}

做一些预处理工作,获取到这个class的 classid 还有对应的一些method。

  1. MM进程连接
class MMActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        Hermes.connect(this)
        ...
      }
}
class Hermes{
  ...
        fun connect(context:Context){
            //创建客服端与服务端的连接
            context.bindService(Intent(context,HermesService::class.java), conn,Context.BIND_AUTO_CREATE)
        }
  ...
}

创建与主进程的连接。

  1. 在MM进程中,通过接口获取代理对象
class MMActivity:AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
      ...
      Handler().postDelayed({
            manager = Hermes.getInstance(IUserManager::class.java)
        },1000)
    }
}
class Hermes{
        fun <T> getInstance(cls:Class<T>):T?{
            val annotation = cls.annotations.filter { annotation -> annotation is ClassId  }.last() as ClassId
            //先直接调用 让另外一个进程创建,这样就建立了 classid-instance的一个关系,后面调用方法的时候,直接通过classid获取到instance
            iHermes!!.sendRequest(Request(TYPE_GET_INSTANCE,annotation.id,"access\$getMInstance\$cp",null))
            return Proxy.newProxyInstance(javaClass.classLoader, arrayOf(cls),HermesInvokeHandler(annotation.id)) as T
        }
}

这里需要做下延时,然后调用Hermes.getInstance,因为bindService获取binder是异步的,Hermes.connect调用后还无法及时的获取到binder。
在getInstance方法中,首先我先利用了AIDL调用了 sendRequest,发送创建单例对象的指令。然后再通过Proxy.newInstance创建IUserManager接口的代理对象返回。接下来在MM进程中调用了这个代理对象的方法都会被我封装成一个Request对象然后调用sendRequest传给主进程,然后主进程执行完方法后把返回对象序列化,返回一个Response

  1. 如何通过sendRequest方法
    首先我们只创建一个AIDL,然后用这个AIDL来表示所有的请求,可以理解为就像Http请求一样,把请求数据封装在一个对象中,也就是Request,然后服务器接收到了这个 Request做好了处理,需要返回一些内容,比如Http请求中的一些返回值。这里使用Response封装了所有的返回内容。

下面是IHermes.aidl,只需要定义一个方法。

package com.javalong.hermes;
import com.javalong.hermes.Request;
import com.javalong.hermes.Response;
interface IHermes {
     Response sendRequest(in Request request);
}

//Service
class HermesService : Service() {
    val instanceMap = ConcurrentHashMap<String,Any>()
    override fun onBind(p0: Intent?): IBinder {
        return object : IHermes.Stub() {
            override fun sendRequest(request: Request): Response {
                //反射调用
                when (request.type) {
                    Hermes.TYPE_GET_INSTANCE -> {
                        val cls = Hermes.getClassById(request.classId) ?: return Response("",false,"")
                        val methods = Hermes.getMethodsByClass(cls) ?: return Response("",false,"")
                        val methodArr = methods.filter { method -> method.name==request.methodName }
                        val obj:Any
                        obj = if(request.param==null|| request.param!!.isEmpty()){
                            methodArr[0].invoke(cls)
                        }else{
                            methodArr[0].invoke(cls,request.param)
                        }
                        //实例存放起来,后面调用实例方法
                        instanceMap.put(request.classId,obj)
                        return Response(Gson().toJson(obj),true,"")
                    }
                    Hermes.TYPE_GET_METHOD -> {
                        val cls = Hermes.getClassById(request.classId) ?: return Response("",false,"")
                        val methods = Hermes.getMethodsByClass(cls) ?: return Response("",false,"")
                        val methodArr = methods.filter { method -> method.name==request.methodName }
                        val instance = instanceMap.get(request.classId)
                        val obj:Any?
                        obj = if(!(request.param!=null&&!request.param!!.isEmpty())){
                            methodArr[0].invoke(instance)
                        }else{
                            methodArr[0].invoke(instance, *request.param!!)
                        }
                        if(obj==null){
                            return Response("",true,"")
                        }
                        return Response(Gson().toJson(obj),true,"")
                    }
                }
                return Response("",false,"")
            }
        }
    }
}

这里给Request分为了2中,一种是getInstance来创建单例,然后把classid-instance对应关联起来。
第二种是调用对象的method方法,先通过classid获取到刚才创建的对象,然后通过反射方法调用,方法调用后获取到返回值,直接封装成Response对象。

所以前面我在返回代理对象前,先调用了第一种方式创建一个单例,不然在后面通过classid就找不到对应的对象了。

  1. 动态代理handler截取请求,跨进程反射调用
class HermesInvokeHandler(val clsId: String) : InvocationHandler {
  override fun invoke(obj: Any?, method: Method, params: Array<out Any>?): Any? {
      var response:Response
      //通过请求创建Request对象,然后调用binder 跨进程调用,获取返回,如果有错误,就直接返回null
      if(params==null) {
           response = Hermes.sendRequest(Request(Hermes.TYPE_GET_METHOD, clsId, method.name,null))
      }else{
           response = Hermes.sendRequest(Request(Hermes.TYPE_GET_METHOD, clsId, method.name,params))
      }

      if (response.source.isNotEmpty()) {
          return Gson().fromJson(response.source, method.returnType)
      }
      return null
  }
}

调用成功后,把Response中的source字符串再转成returnType类型。然后返回。

难点分析

  1. 不熟悉aidl的可以先借此机会练手,先直接使用aidl进行通讯,然后实现了再来进行封装,这样会比较容易。

  2. 这里比较容易搞混的是动态代理和跨进程中binder里面的处理。首先我们要理解,MM进程中首先调用的是代理对象,所以一开始进的,是代理对象的InvocationHandler,然后在InvocationHandler里面再利用AIDL去跨进程调用,然后才会运行到HermesService里面的方法,然后执行完成之后返回 Response又回到了InvocationHandler里面了。这个顺序需要搞清楚。

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

推荐阅读更多精彩内容