面向全栈在项目中踩过的坑程序员

Wax源码简析

2017-05-22  本文已影响119人  子达如何

阅读的是阿里维护的Wax版本

先针对如下用法的实现过程进行讲解。

用法

waxClass{"ViewController", UIViewController}
function rewriteFunction(self)
    self:ORIGrewriteFunction()
    wax.print("replaced function")
end

用法讲解

详细的Wax用法讲解,请参阅https://github.com/alibaba/wax/wiki/Overview
下面只把阅读源码相关的一些用法提要说明一下:

  1. waxClass第一个参数是类的名字(字符串)"ViewController",第二个参数是它的父类名字(符号,不是字符串)UIViewController;
  2. 接着定义一个函数,这个函数就是你要改写类ViewController的函数,它的方法名和OC里定义的方法名要一致,第一个参数是self,含义同OC里的self;
  3. 在OC执行rewriteFunction函数之前加载这段lua脚本,然后OC里执行到rewriteFunction这个方法的时候,就自动的执行lua里定义的这个函数了。

实现讲解

  1. waxClass是一个定义在stdlib里的全局函数(代码是用lua写的),它首先调用wax.class创建出这个类的实例(instance),然后创建一个元表,再把元表设置为当前的运行时环境。
    这个元表的设计有点巧妙,使得waxClass之后的函数定义都相当于是定义在这个类实例里面的函数。
    所以,你就能看到一个类定义和一个函数定义是怎么关联起来的——虽然,代码上他们是互相独立的东西。
    如果没有了这个运行时环境的元表定义,那么,上面的代码就得改成这样的写法了:

    local myclass = waxClass{"ViewController", UIViewController}
    myclass.rewriteFunction = function(self)
        self:ORIGrewriteFunction()
        wax.print("replaced function")
    end
    

    也就是需要显式的把类实例和函数定义关联起来。个人认为这种显式的关联写法,理解上更加好一些。

  2. UIViewController这个符号有点奇怪,它看上去是lua的全局符号,但是,如果调试代码的话你会发现,它最终触发的是引擎wax.class里注册的__index方法。秘密其实就在stdlib的init.lua里:

    setmetatable(_G, {
      __index = function(self, key)
        local class = wax.class[key]
        if class then self[key] = class end -- cache it for future use

        if not class and key:match("^[A-Z][A-Z][A-Z][^A-Z]") then -- looks like they were trying to use an objective-c obj
          print("WARNING: No object named '" .. key .. "' found.")
        end

        return class
      end
    })
```
    它在lua的全局环境中加了元方法,使得,所有lua未定义的符号都转成访问wax.class[key]。
3. 上面说到waxClass内部是调用的wax.class,调用wax.class就对应到引擎里wax_class.m的``static int __call(lua_State *L)``方法调用了。
这一步没有什么特别的,就是创建一个类实例,这个类实例在引擎里就是一个``wax_instance_userdata``的user data。
对于上面的实例代码,看显式关联的代码就很清楚了,这个类实例就是``local myclass``。
此外,这里如果你不是打算改写OC现有类的话,你完全可以定义一个全新的类,这个新的类甚至可以被OC调用(置于OC怎么知道这个类的存在.....)。

4. 接下来再看函数的定义,第一步已经分析过了,函数的定义被修改过的运行时环境限定到了类实例里了,因此这里定义一个函数就会触发引擎wax_instance.m里的``static int __newindex(lua_State *L)``。这个函数很简单,就是把key,value登记到表里面,然后做一个最重要的事情方法改写``overrideMethod``

5. overrideMethod的函数实现很长,先说一个处理上比较绕的点:lua定义个方法名可能对应到OC的"无参数"和"一个参数"的情况。
比如,OC中如此定义:

```
- (void)onearg:(int)a {
    NSLog(@"real one arg");
}
- (void)onearg {
    NSLog(@"overloaded one arg");
}
```
在Lua你都是这样定义
```
function onearg(self)
end
```
lua里的这个定义,你是无法区分要重写一个参数的OC函数还是重写无参数的OC函数。
因此,你会看到很多地方都要考虑这种情况,并且当前的Wax版本还无法做到分别改写这两个方法,幸好实际应用中这种情况不多见。(我自己提交了一个PR支持分别改写这两个方法的方案)。

然后,还要在补充一下lua改写OC方法的规则,对于无参数和一个参数的方法,直接定义OC的方法即可。一个参数以上的,参数和方法之间/参数与参数之间用下划线'_'分割,例如
```
OC
- (void)test:(int)a b:(int)b{
    NSLog(@"test");
}
Lua
function test_b(self, a, b)
end
```
如果方法本身有下划线'_'话就恶心了,要替换成好长的一个符号"UNDERxLINE",例如
```
OC
 (void)_prefixA:(NSString *)a _b:(NSString *)b
Lua
function UNDERxLINEprefixA_UNDERxLINEb(self, a, b)
    self:ORIGUNDERxLINEprefixA_UNDERxLINEb(TEST_VALUE_STRING, TEST_VALUE_STRING)
end
```
---

## 重点讲解一下overrideMethod的实现过程
1. 第一步是找到方法名对应的selector,因为lua里函数对应到OC的多参数方法是通过加下划线实现的,因此在方法名和selector之间有一些转换,认真读一下代码就能理解了。
* 找到selector之后,通过class_getInstanceMethod获取method。如果在当前类找不到这个method的话,就沿着继承树向上查找。
* 如果最终在某个地方找到了method的话,则通过method_getTypeEncoding获得type encoding,method_copyReturnType获得return type。并最终执行overrideMethodByInvocation。
* 如果最终也找不到这个method的话,那么,尝试看看这个方法是不是类方法?如果是的话最终执行overrideMethodByInvocation。
* 如果也不是类方法,那么就执行addMethodByInvocation。注意,它同时定义了实例方法和类方法。(因为,lua的函数定义并不能很好的区分是实例方法,还是类方法)。
* overrideMethodByInvocation首先把@selector(forwardInvocation:)的实现替换成waxForwardInvocation,然后把原来的这个selector改名成WAXORIG开头的selector,设置它的实现为原select的实现实现。
这个处理本质上就是方法调换,@selector(forwardInvocation:)和@selector(AXORIGforwardInvocation:)的一个对调。
最后,类似的手法,把我们正在处理的selector的实现替换成了``IMP forwardImp = _objc_msgForward``。
这里的替换有点小复杂。
我把替换之后的结果说一下,以刚开始举例的代码说明:
当OC调用rewriteFunction的时候,由于rewriteFunction的实现已经被替换成了``_objc_msgForward``,于是触发了消息转发,消息转发在runtime里的处理就是调用``forwardInvocation:``,又由于``forwardInvocation:``已经被替换成了``waxForwardInvocation``,于是就是调用到waxForwardInvocation了。OC的一个方法调用成功落入到我们自己实现的一个方法了,并且得到了NSInvocation这个有用的参数,它把调用的参数,返回值类型等等信息都一一带过来了,非常好。图示如下:
```OC--->rewriteFunction--->_objc_msgForward--->forwardInvocation--->waxForwardInvocation```
* ``static void waxForwardInvocation(id self, SEL sel, NSInvocation *anInvocation)``简单做了一下判断,如果是我们替换过的方法,那么就执行pcallUserdataARM64Invocation
* pcallUserdataARM64Invocation 就是根据selector的签名信息,把NSInvocation里的各个参数信息压入lua的栈,然后执行lua的函数,并且把函数的执行结果设置到NSInvocation的返回值。于是OC对一个函数的调用就被完美的转移到lua的函数执行,并返回执行结果给OC。

### 最后补充一下调用原方法的实现过程
```
self:ORIGrewriteFunction()
```
前面已经讲过了类instance设置了__index的元方法,lua在解析到上面这个调用的时候,就会落入wax_instance.m的__index函数。__index经过一系列处理之后,最终会把方法的符号(ORIGrewriteFunction)和一个methoClosure关联起来。然后,执行self:ORIGrewriteFunction就是执行methodClosure。类似的技巧在结构体里也用到了。
这个methodClosure有两个闭包upvalue,分别是两个可能的selector的名字。
这个methodClosure做的事情就是给NSInvocation设置各种参数,然后执行即可。

## 对结构的支持
Wax对结构体的支持分两步进行,第一步是定义结构体。这个定义在stdlib的struct.lua里定义了。类似这样的:
```
wax.struct.create("CGSize", "dd", "width", "height")
wax.struct.create("CGPoint", "dd", "x", "y")
```
1. 在Lua导出了一个wax.struct的表,注册一个create方法,类似下面的这个语句,就是创建一个叫CGSize的结构,类型是两个double类型的数据(用dd表示),结构的两个字段名字分别是width和height
    wax.struct.create("CGSize", "dd", "width", "height")

2. wax.struct的元表下有一个以LABELED_STRUCT_TABLE_NAME为key的表,用来登记所有已经create了的结构体的信息。这个表以结构体的名字作为key,其值记录了结构体的字段名字对应的索引位置。
以上面的create为例,create之后的lua栈内就有了这么一个表结构形态:
``` wax.struct {
        LABELED_STRUCT_TABLE_NAME : {
            "CGSize": {
                "w": 1,
                "h": 2
            }
        },
        __index: function,
        __newindex: function,
        __gc: function,
    }
```
3. 然后在全局空间用结构名为key登记一个闭包``createClosure``,这个闭包就是用来实现创建结构体对象的,如:``let size = CGSize(1,2)``
这个语句由于CGSize是闭包,因此lua会执行这个闭包。注意到,这个闭包创建的时候,会把结构体类名和类型信息设置成了闭包的upvalue了,从而,在闭包的执行过程就可以访问到这两个信息了。

4. createClosure创建一个wax_struct_userdata,再根据类型信息,把参数的值编码(pack)成一个Buffer,设置为structUserData->data中保存。

5. 当lua代码访问结构变量下标(通过索引或者字段名)的时候就可以从这个wax_struct_userdata恢复出需要的数据来了。阅读代码的时候,你会看到之前第2步登记的字段名和字段位置就是为了在这里根据索引定位数据使用的。

注意:调试的时候发现__gc老不被执行,总觉得是内存泄露了。其实,并不是,lua的GC默认是在内存增长到上一次GC之后的两倍之后才会触发一次。

## dispatch以及C API的支持
这些都在extersions里面的capi目录里,通过tolua++实现。tolua++就不用多分析了,是官方的一个把C/C++导出给lua使用的工具和库。

#lua传递block参数给OC
假设类ViewController有一个方法methodWithBlock需要一个block做参数。
那么,lua调用的时候要通过toblock这个函数处理,并且还要提供返回值和参数类型的信息。
```
OC:
@implementation ViewController
- (void)methodWithBlock:(int (^)(int a))block {
    NSLog(@"block return:%@", @(block(1)));
}
Lua:
local vc = ViewController:init()
vc:methodWithBlock(
    toblock(
        function(a)
            return a + 1
        end
        ,
        {"int", "int"}
    )
)
```
具体的实现过程是这样的:
首先,在stdlib的ext/block.lua里定义了一个toblock的函数。
这个函数的第一个参数是一个标准的lua function
通过调用```toobjc(func)```,会得到一个WaxFunction的对象实例,然后根据是否有参数,分别调用这个实例的不同方法:
``luaVoidBlock()``或者``luaBlockWithParamsTypeArray()``
这两个方法都是返回一个OC的block。前面已经讲过,调用WaxFunction类对象的方法,实际上是触发methodClosure。
methodClosure在处理返回值的时候,会调用wax_fromObjc,由于block其实就是一个OC的对象,于是会调用wax_fromInstance创建一个对象的instance。然后,lua在执行到有block做参数的函数的时候,就能从对象的instance得到block,传递给函数执行了。

## 64位和32位的各种兼容性的处理
阿里采用的方法是分别编译不同的lua脚本。
我自己觉得这个方案有点啰嗦,不如直接改一下引擎来得直接。详细请参阅另一个文章http://www.jianshu.com/p/3c49cf454502
上一篇下一篇

猜你喜欢

热点阅读