DynamOC一个iOS APP热更新轮子

达芬奇密码2018-07-25 11:08

前言

前段时间使用了JSPatch热跟新框架的APP都收到了Apple的警告信,开发者们在JSPatch的github项目主页上的Apple警告邮件 issue中展开了热烈的讨论。开发者bang590建议先不要使用JSPatch,现在时间已经过去了个把个月,在JSPatch跟新平台跟新公告中说他们已经提出了解决方案,使用JSPatch不会被Apple拒绝。易信项目也接入过JSPatch,修复了好几个线上Bug,但在使用过程也碰到了几个JSPatch的不足之处,遂萌生了自己实现一个热跟新框架的想法。

DynamOC Repo

DynamOCTest Repo

注意:目前由于LuaJIT的原因,只能在真机上运行,有编译过可以运行在模拟器上的LuaJIT的同学可以私下交流下

JSPatch的不足

JSPatch使用JS作为开发语言,JavaScriptCore为JS何OC提供了方便的互通机制,但同时也存在着一些不足:

  1. 不支持64位整型数据

    JS中的数值类型都存储为double,将OC中64位中整型数据传到JavaScriptCore中可能会导致精度损失。

  2. JS代码无法并发执行

    JSVirtualMachine文档中说avaScriptCore API是线程安全的,这个线程安全是通过JSVirtualMachine的全局锁来实现的,即JSVirtualMachine是不支持并发操作的。如果想要实现并发执行JS代码可以通过创建多个JSVirtualMachine的实例,但是JSValue是不能在JSVirtualMachine之间传递的。JSPatch虽然提供了GCD API但是JSVirtualMachine的限制从根本上决定了这些GCD API并不能实现并发执行JS代码。(就算JSPatch创建多个JSVirtualMachine实例来运行JS代码,也不能实现GCD的完整功能)

DynamOC的语言选择

DynamOC选择Lua作为开发语言,但没有使用官方的实现,而是采用LuaJIT的实现。选择Lua语言和LuaJIT主要基于以下几点考虑:

  1. LuaJIT提供了强大的FFI扩展,有了FFI就可以方便的访问OC的runtime API
  2. LuaJIT提供了原生的64位数据支持
  3. Lua语言的debug库使得DynamOC的并发执行成为可能

DynamOC的使用

1. OC方法调用

方法名转换

在lua中:是关键字(表示调用的方法的第一个参数是调用者),所以OC方法中的:都要用_代替

local indexPath = runtime.NSIndexPath:indexPathForRow_inSection_(0, 1)

若OC原方法名中包含_,在lua中使用__代替

类方法调用

使用runtime.className获取OC中的class

local color = runtime.UIColor:redColor()
实例方法调用

local view = runtime.UIView:alloc():init()

view:setNeedsLayout()
父类方法调用

runtime.callSuper(instance, cmd, ...)
Property

调用property的getter和setter方法来获取/设置property的值

view:setBackgroundColor_()

2. class相关操作

创建class

runtime.createClass(superClass, className, iVar, properties)

@param superClass: OC class,父类, runtime.className

@param className: 字符串,类名

@param iVar: 字典

{varName = "typeEncoding"}

@param properties: 字典

{    propertyName = 
    {
        typeEncoding    = "typeEncoding", 
        readOnly         = true | false, 
        ownerShip        = "weak|strong|copy", 
        nonatomic     = true | false,
        setterName    = "setterName",
        getterName    = "getterName",
        ivarName        = "ivarName"
    }
}

内置了一些常用的typeEncodeing:

runtime.encode.id = "@"
runtime.encode.Class = "#"
runtime.encode.SEL = ":"
runtime.encode.block = "@"
runetime.encode.void = "v"
runtime.encode.CGFloat
runtime.encode.NSInteger
runtime.encode.NSUInteger
runtime.encode.CGSize
runtime.encode.CGRect
runtime.encode.UIEdgeInsets

可以通过lua语言中的..字符串连接操作符自由组合

如果不设置ownerShip属性则默认为assign,如果不设置setterName,则默认为setPropertyName,如果不设置getterName,则默认为propertyName,如果不设自ivarName,则默认为_propertyName

添加/覆盖方法

runtime.addMethod(class, selector, lambda, methodEncoding)

@param class: OC class,runtime.className

@param selector: OC selector, runtime.SEL("selectorName")

@param lambda, lua function, 添加的方法定义

function(self, cmd, ...) 
    ...
end

需要注意OC方法有两个隐藏参数,分别是第一个参数方法调用者和第二个参数调用的调用方法的SEL,所以methodEncoding要加上这两个隐藏参数,lambda定义的第一和第二各参数必须是self和cmd

@param methodEncoding: 字符串,方法的type encoding,例如"v@:",表示一个第一个参数是id,第二个参数是SEL,返回值为void的方法

如果添加的方法不存在则创建该方法,如果已存在则覆盖原方法

读取/设置iVar

runtime.getIvar(instance, iVarName)
runtime.setIvar(instance, iVarName, value)

3. Block

将lua function封装成OC Block,需要调用:

runtime.createBlock(lambda, methodEncoding)

array:enumerateObjectsUsingBlock_(runtime.createBlock(function(str, index, stop)
    cocoa.NSLog(runtime.Obj("%@"), str)
end, runtime.encode.void..runtime.encode.id..runtime.encode.id..runtime.encode.NSUInteger.."^B"))

需要注意OC Block调用有一个隐藏参数,表示的是这个block方法的调用者,即这个block本身(类型为NSBlock *),所以methodEncoding要加上这个隐藏参数,跟方法定义不同,我们不会用到这个隐藏参数,所以lambda的定义不需要加上这个隐藏参数

4. __weak和__strong

local weakSelf = runtime.weak(self)
local strongSelf = runtime.strong(weakSelf)

5. NSArray、NSString、NSNumber, NSDictionary

local array = runtime.Obj({1, 2, 3})
local string = runtime.Obj("hello")
local dict = runtime.Obj({key = value})
local number = runtime.Obj(1)

6. GCD

目前只实现了dispatch_async和dispatch_sync

dispatch.async(dispatch.mainQueue, function()
    ...
end)

dispatch.async(dispatch.get_global_queue(0, 0), function()
    ...
end)

dispatch.sync(dispatch.mainQueue, function()
    ...
end)

dispatch.sync(dispatch.get_global_queue(0, 0), function()
    ...
end)

7. 指针

指针的typeEncoding为"^"加上所指类型的typeEncoding,对于特定的指针比如常见的BOOL 的typeEncoding为"^B",NSError\*的typeEncoding为"^@"

在lua中将指针理解为数组的首地址来访问存储所指向的值,比如:

function(str, index, stop)
    ...
    stop[0] = true
end

stop的类型为BOOL *stop

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