热跟新框架DynamOC实现原理

达芬奇密码2018-07-23 09:57

用法简介

基础用法

Objc的方法调用方式换成了类C++方式的方法调用,比如:

[[UIView alloc] initWithFrame:frame]

在DynamOC中写成:

runtime.UIView:alloc():initWithFrame_(frame)

因为":"在lua中是一个关键字,所以Objc方法命中的":"都替换成"_"。"."为属性访问,":"为面向对象的方法调用。

常用方法

动态创建class

runtime.createClass(runtime.UIView, "DynamView",
 {_iVar = "@"}, 
 {btn = {typeEncoding = "@", ownerShip = "strong"}})

第一个参数是父类,第二个参数是创建的类名,第三个参数是实例成员变量的定义,第四个参数是property的定义

替换及增加类方法

runtime.addMethod(
    runtime.DynamView, 
    runtime.SEL("initWithFrame:"), 
    function(self, cmd, frame) 
        self = runtime.callSuper(self, cmd, frame)
        if self then
        end
    return self
end, "@@:{CGRect={CGPoint=dd}{CGSize=dd}}")

最后一个参数为method signature,如果是替换方法可以不传

原理介绍

基础原理

使用Objc的runtime,我们可以使用类名及方法名得到相应的类及方法。以下面这行代码为例:

runtime.DynamView:alloc():initWithFrame_(frame)

runtime.UIView内部调用objc_getClass来得带该类。而所有的方法调用内部都调用了objc_msgSend,上面的Objc代码转成C伪代码:

viewClass = objc_getClass("DynamView");
view = objc_msgSend(viewClass, sel_registerName("alloc"))
objc_msgSend(view, sel_registerName("initWithFrame:"), frame)

这里面还有一些细节比较麻烦,比如在64位系统下使用objc_msgSend就可以了,而在32位系统下根据方法返回值的不同需要调用objc_msgSend_stret

方法替换

方法替换的实现需要绕点弯子,在这之前我们先要了解objc_msgSend内部的流程,这里用最简单的代码来实现objc_msgSend函数:

id objc_msgSend(id self, SEL _cmd, ...)
{
    Class c = object_getClass(self);
    IMP imp = class_getMethodImplementation(c, _cmd);
    return imp(self, _cmd, ...);
}

我们只要使用method swizzling把想要替换的方法的IMP替换掉就能实现方法替换。DynamOC基于LuaJIT实现,之所以有JIT这个后缀,就是因为它能把lua代码运行时编译成机器码执行。但是,iOS上是不允许JIT的,所以LuaJIT在iOS上只能解释执行lua代码,简单的把lua代码编译成机器码,然后用该编译的代码替换目标IMP是行不通的。幸运的是Objc还有一个消息转发机制,我们用调用一个未实现的方式的处理流程来介绍下该机制:

  1. [object unknownMethod]
  2. runtime调用object的methodSignatureForSelector:方法询问unknownMethod的method signature
  3. 如果methodSignatureForSelector:返回nil,抛出异常,否则执行4
  4. 如果methodSignatureForSelector:返回了method signature,则runtime根据该signature将该方法调用的信息比如参数,返回值地址,打包成一个NSInvocation
  5. runtime调用object的forwardInvocation:方法

从上述的流程我们可以看到在方法forwardInvocation:中我们可以得到方法调用的一切信息,包括最重要的参数,返回值地址。所以在DynamOC中方法替换的实现途径就是让被替换的方法最终能够调用到forwardInvocation:方法。为了实现这个我们需要用到一个最关键的runtime函数_objc_msgForward,该函数的作用就是把调用该函数的参数打包成一个NSInvocation对象,然后调用 forwardInvocation:方法。现在实现的思路就很清晰了:

  1. 使用_objc_msgForward来替换目标方法的IMP
  2. 使用自定义forward_invocation函数替换forwardInvocation:的IMP
  3. 在forward_invocation中调用lua代码,并将返回值返回

与JSPatch比较

JSPatch优点

js比lua的普及程度更高

JSPatch的缺点

  1. 无法并发执行
  2. 对于block的支持有限制,JPBlock参数不支持struct / union
  3. 对C函数的调用支持有限制,JPCFunction的参数不能是struct 类型和 block 类型
  4. 不支持64位整形数据,这是JS语言的限制

第2和第3点JSPatch的wiki里说的,而第1点并没有说明,虽然JSPatch提供了一些GCD的global queue aync dispatch方法,但是其实都是串行执行的。下面代码一测便知:

dispatch_async_global_queue(function(){
      while(true) {
          console.log('thread1');
      }
})
dispatch_async_global_queue(function(){
      while(true) {
          console.log('thread2');
      }
})

JSPatch无法并发执行,是由于JavaScriptCore的限制造成的,在JSPatch中有一个全局的JSContext用来执行的js代码,每个JSContext都隶属于一个JSVirtualMachine,根据Apple文档:

Each JavaScript context (a JSContext object) belongs to a virtual machine. Each virtual machine can encompass multiple contexts, allowing values (JSValue objects) to be passed between contexts. However, each virtual machine is distinct—you cannot pass a value created in one virtual machine to a context in another virtual machine.

The JavaScriptCore API is thread safe—for example, you can create JSValue objects or evaluate scripts from any thread—however, all other threads attempting to use the same virtual machine will wait. To run JavaScript concurrently on multiple threads, use a separate JSVirtualMachine instance for each thread.

也就是说JSVirtualMachine中的锁限制了并发。那JSPatch是否能像文档里说的那样创建多个JSVirtualMachine实例来达到并发执行呢?文档里也说了JSVirtualMachine之间是不能传递JSValue的,这就导致GCD中的global queue aync dispatch是无法实现的

DynamOC

DynamOC可以并发执行,原生支持64位整型数据,也不存在JSPatch的诸多限制,想比JSPatch现在一个缺点是无法在模拟器上运行,这个主要是LuaJIT在模拟器上的编译还有点问题,还在研究解决

不可描述的事情

动态加载framework

runtime.loadFramework("Accounts", false)
local store = runtime.ACAccountStore:alloc():init()
local accounts = store:allAccountTypes()

调用任意系统API

runtime.ffi.cdef[[
    unsigned int sleep(unsigned int);
]]
runtime.ffi.C.sleep(10)

项目集成

DynamOC只提供了一个热跟新机制,补丁的发布使用邮箱的发布系统NESky,整个系统名字为NEPatch,使用pod就能集成

source 'ssh://git@g.hz.netease.com:22222/ntes-onepiece-ios/CocoaPodsSpecs.git'
pod 'NEPatch'

在AppDelegate做些配置:

[NEPatch setupRSAPublicKey:WWNEPatchKey];
[NEPatch setupDeviceID:[WWDeviceManager sharedManager].deviceID];
[NEPatch setupChannel:[WWAppConfigManager sharedManager].channel];
[NEPatch setupLogHandler:^(NEPatchLogLevel level, const char* fullpath, int line, const char* prefix, NSString* content) {
    NSUInteger length = strlen(fullpath);
    NSString* fullPath = [[NSString alloc] initWithBytesNoCopy:(void*)fullpath length:length encoding:NSUTF8StringEncoding freeWhenDone:NO];
    NSString* fileName = [fullPath lastPathComponent];
    switch (level) {
        case NEPatchLogLevelError:
            DDLogError(@"%zd [%s] %@ [%@:%d]", level, prefix, content, fileName, line);
            break;
        case NEPatchLogLevelInfo:
            DDLogInfo(@"%zd [%s] %@ [%@:%d]", level, prefix, content, fileName, line);
            break;
        case NEPatchLogLevelDebug:
            DDLogDebug(@"%zd [%s] %@ [%@:%d]", level, prefix, content, fileName, line);
            break;
        case NEPatchLogLevelWarn:
            DDLogWarn(@"%zd [%s] %@ [%@:%d]", level, prefix, content, fileName, line);
            break;
        default:
            break;
    }
}];
[NEPatch run];

本文来自网易实践者社区,经作者徐晖授权发布。