文章

iOS的lua热修复详解

介绍

语言介绍

lua和python

  • 脚本语言中运行速度最快的是 Lua,lua是基于寄存器的虚拟机实现(更简单,更高效),python是基于堆栈的,都是动态数据类型
  • python有自己的库,是基于自身独立开发的,lua离开c/c++的话没法开发,lua更类似是一层封装
  • lua,python都是解释型语言

lua和c

  • c和lua的交互关键是虚拟栈
  • 轻量小巧的脚本语言,用C编写并源码开放,设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
  • 轻量:编译后只有一百余k
  • 嵌入式脚本语言
  • 高可扩展:Lua可使用C,扩展能力由C的接口提供
  • lua支持面向过程和函数式编程,C面向过程编程
  • lua自动内存管理,使用自动垃圾收集机制,C要求显示的分配和释放内存
  • lua使用动态数据类型,C使用静态类型

向前辈学习

wax只支持32位,线程保护不好,而且最重要的是已经停止维护

SPA介绍

spa是参考wax的实现原理以及lua的使用,又目前在职的邮箱大师范飞重写的,目前支持仅支持方法替换式的修复,其他能力有待完善,愿天下开发远离bug。

本篇章主要讲解下实现原理。

原理介绍

OC—->SPA

1
2
3
4
5
6
7
8
9
10
11
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)doSomeThing {
    self.view.backgroundColor = UIColor.grayColor;
}

- (void)viewDidLoad {
	[self doSomeThing];
}
@end

SPA

1
2
3
4
spa_class("ViewController")
function doSomeThing(self)
	self:view():setBackgroundColor_(UIColor:grayColor())
end

读完本篇章之后,希望你对下面三个问题能有点头绪。

  1. vc替换doSomeThing这件事,lua怎么传递给OC那边的?
  2. 当OC在执行doSomeThing的时候,如何执行到lua实现的版本的?
  3. lua调用函数的时候,又如何转换成调用OC的函数?

lua特性介绍

lua菜鸟教程

zerobrane IDE下载

安装并打开zerobrane,第一步先来熟悉下lua的比较重要的两个特性,元表和环境

setmetatable 元表

Lua table(表)

1
2
3
4
5
6
7
8
local t = {
  a = 'avalue'
  }
print(t.a) -- avalue
print(t['a']) -- avalue

t.a = 'newvalue'
print(t.a) -- newvalue

是不是很像字典的key-value,点的用法就是类似oc的get-set

setmetatable使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local fan = {}
print(fan.money) -- nil

local mis = { money = 100}
print(mis.money) -- 100

local _M = { __index = mis }
setmetatable(fan, _M)

print(fan.money) -- 100
fan.bonus = 10

_M = { __newindex = function(t, k, v) 
      mis.bonus = mis.money + v
    end}

setmetatable(fan, _M)
fan.bonus = 200

print(fan.bonus) -- nil
print(mis.bonus) -- 300

元表有两个最重要的特性,__index__newindex

  1. 当get表中不存在某个key的时候,会转移到设置的元表中__index所指向的地方
  2. 当set一个不存在key的值的时候,会转移到关联的元表的__newindex中,当然key对应的value可以是function

所以这里就相当于是hook了,function三个参数比较关键,1 table 2 key 3value

元表中还有其他,比如说__call __add __sub __eq 帮我们做一些很高级的事情 比如想调用fan(),这个时候就会报错调用不了

1
2
3
4
5
local _M = { __call = function() 
      print("fan")
    end}
setmetatable(fan, _M)
fan() -- fan

setfenv 环境

环境类似当前代码块的作用域?直接举例子说吧

lua有个全局的环境叫_G,是全局的,其实我们上面在调用print的时候,默认是_G.print

1
2
3
4
local t = {
  a = 'avalue'
  }
_G.print(t.a) -- avalue

通过使用setfenv,来更改方法调用的环境

置空全局环境:

1
2
setfenv(1, {}) -- 1改变当前作用域的环境 2 改变外层作用域
print('a') -- Execution error: attempt to call global 'print' (a nil value)

方法替换中的关键,获取被替换的方法名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function f()
  local t = {}
  setmetatable(t, { __newindex = function (t,k,v) 
        print(k)
        end})
  setfenv(2, t)
end

f() -- 先将外层环境设空,然后关联一个元表,并赋值__newindex,这样在该环境中赋值的时候

function doSomeThing() -- 控制台直接打印doSomeThing
  
end

-- 其实就是类似 _G.doSomeThing = function() end就会走到__newindex中

lua api介绍

c调用lua

比如lua写了一个add的方法,通过c实现的lua api,luaL_loadfile将lua脚本,加载到lua的虚拟机里面,这样lua在全局的环境里面就有了一个add的方法 然后在c里面通过lua api:lua_getGloable找到这个添加的add方法,然后lua_push添加执行参数,最后用lua_pcall进行执行 (栈原理)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// test.lua
function add(x, y)
	return x+y
end

//test.c
#include <stdio.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
#include <math.h>

int main() {
	lua_State *L = luaL_newstate();
	luaL_openlibs(L);

	if(luaL_loadfile(L, "test.lua") || lua_pcall(L,0,0,0)) {
		printf("error %s\n", lua_tostring(L, -1));
		return -1;
	}
	lua_getglobal(L, "add");
	lua_pushnumber(L, 10);
	lua_pushnumber(L, 20);
	if (lua_pcall(L, 2, 1, 0) != 0) {
		printf("error %s\n", lua_tostring(L, -1));
		return -1;
	}
	double z = lua_tonumber(L, -1);
	printf("z = %f \n", z);
	lua_pop(L, 1);
	lua_cloase(L);
	return 0;
}

图片

lua调用c

比如首先我们要有一个c的函数在,myadd,在lua里面要调用的话,首先需要注册到lua的虚拟机里面,luaL_newlib,luaL_register方法 lua中注册c函数的固定格式 typedef int (*) (lua_State *)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// mylib.c
#include <stadio.h>
#include <lua.h>
#include <lualib.h>
#include <luaxlib.h>
#include <math.h>

static int myadd(lua_State *L) {
	int a = luaL_checknumber(L, 1);
	int b = luaL_checknumber(L, 2);
	lua_pushnumber(L, a+b);
	return 1;
}

static const struct luaL_Reg mylib [] = {
	{"add", myadd},
	{NULL, NULL}
};

int luaopen_mylib(lua_State *L_ {
	luaL_newlib(L, mylib);
	return 1;
}

//call.lua
#!/usr/local/bin/lua
lib = require "mylib"
print(lib.add(1,2))

SPA调用

1
2
3
4
5
spa_class("ViewController")

function doSomeThing(self)
	self:view():setBackgroundColor_(UIColor:grayColor())
end

首先看第一行 spa_class(“ViewController”)

usePatch

spa_class就是我们预装载在lua虚拟机里面的的lua方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function spa_class(class_name)
    local class_userdata = spa.class.create(class_name)

    local _M = {self = class_userdata}

    setmetatable(_M, {
        __newindex = function(self, key, value)
            class_userdata[key] = value
        end,
        
        __index = function(self, key)
            return spa.class.create(key) or class_userdata[key] or _G[key]
        end,
    })

    setfenv(2, _M)

    return class_userdata
end

改脚本被预先编译成了二进制文件,详细见spa_stdlib.h

spa_class的lua方法预装载就在usePatch方法中,所以每一次注入脚本之前就会执行这个预装载逻辑

1
2
3
4
5
6
7
8
9
// Spa.m
- (void)usePatch:(NSString *)patch {
	···
    char stdlib[] = SPA_STDLIB;
    size_t stdlibSize = sizeof(stdlib);
    
    if (luaL_loadbuffer(L, stdlib, stdlibSize, "loading spa stdlib") || lua_pcall(L, 0, LUA_MULTRET, 0)) {
	···
}
spa_class

回头看到spa_class的调用

1
2
function spa_class(class_name)
    local class_userdata = spa.class.create(class_name)

入参为需要修复的类名,然后走到create中,create就是lua->c,提前注入到lua虚拟机中的c方法

我来教你一步步看create在哪 起点就是上面介绍的userPatch的下面几句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Spa.m userPatch:
[self setup:L];

// Spa.m setup:
[self.spa_class setup:L];

// SpaClass.m setup:
luaL_register(L, SPA_CLASS, Methods);

// SpaClass.h 
#define SPA_CLASS "spa.class" // lua class module

// SpaClass.m
static const struct luaL_Reg Methods[] = {
    {"create", create},
    {"recoverMethod", recoverMethod},
    {NULL, NULL}
};

static int create(lua_State *L)
{
    const char* klass_name = lua_tostring(L, 1);
    /// 以需要修复的类名创建一个userdata(lua中是table,key-value;c中就是一个结构体指针)
    return [SpaClass createClassUserData:L klass_name:klass_name];
}

接着看spa_class的方法体,是不是下面的代码很熟悉,没错,用的就是我们上面介绍的lua的环境特性:将当前spa_class的调用环境设置为_M,_M是设置了元表(__index和__newindex),并且只有一个key-value的表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function spa_create(class_name)
  local class_userdata = {
      classname = class_name
    }
  
  local _M = { self = class_userdata }
  
  setmetatable(_M, {
        __newindex = function(table, key, value)
          print(key) -- doSomeThing
          print(value) -- function: 0x02ec57d0
          end,
      
        __index = function(self, key)
        	-- 因为调试环境没有注入spa.class.create 先注释
            -- return spa.class.create(key) or class_userdata[key] or _G[key]
        end,
      })
  
  setfenv(2, _M)
end

spa_create('ViewController')
function doSomeThing(self)
end

这里我们就拿到了需要替换的方法名和方法体

再看第二行 function doSomeThing(self)

走到的就是上一小节提到的_M中的__newindex中

class_userdata[key] = value

进入到__newindex中的class_userdata[key] = value,这里是不是很熟悉,又是我们表的set

我们回到class_userdata的创建的地方,看看是不是给class_userdata设置了元表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SpaClass.m createClassUserData:klass_name:
size_t nbytes = sizeof(SpaInstanceUserdata);
SpaInstanceUserdata *instance = (SpaInstanceUserdata *)lua_newuserdata(L, nbytes);
instance->instance = klass;

luaL_getmetatable(L, SPA_CLASS_META_TABLE);
lua_setmetatable(L, -2); // lua api的设置userdata的元表为SPA_CLASS_META_TABLE

lua_newtable(L);
lua_setfenv(L, -2); // 并且设置环境

// 全局搜索SPA_CLASS_META_TABLE
// SpaClass.m setup:
luaL_newmetatable(L, SPA_CLASS_META_TABLE);
luaL_register(L, NULL, MetaMethods); // class_meta_table指向的就是MetaMethods中的__index和__newindex
// MetaMethods
static const struct luaL_Reg MetaMethods[] = {
    {"__index", __index},
    {"__newindex", __newIndex},
    {NULL, NULL}
};

所以class_userdata[key] = value最后走到

1
2
// SpaClass.m __newIndex
static int __newIndex(lua_State *L) // 方法替换逻辑
方法替换

这里和jspatch的方法替换就很像了

SpaClass.m __newIndex,首先判断是否可以替换,然后进行方法替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void replaceMethod(Class klass, SEL sel)
{
    if (klass == nil || sel == nil) {
        return ;
    }
    SEL originSelector = spa_originForSelector(sel);
    Method targetMethod = class_getInstanceMethod(klass, sel);
    if (targetMethod) {
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        // 将原方法挂在ORIGdoSomeThing上,这样我们可以使用ORIG实现调用原方法
        class_addMethod(klass, originSelector, method_getImplementation(targetMethod), typeEncoding);
        // 使用forwardInvocation去hook
        spa_swizzleForwardInvocation(klass);
        // 将替换的方法doSomeThing直接挂在消息转发的imp上,这样一旦调用直接走进消息转发
        class_replaceMethod(klass, sel, spa_getMsgForwardIMP(klass, sel), typeEncoding);
        // 方法替换复原使用
        [[SpaClass replacedClassMethods] addObject:@{@"class":NSStringFromClass(klass), @"sel":NSStringFromSelector(sel)}];
    }
}

这样一旦触发调用,就会走进hook的消息转发的方法中

1
2
// SpaClass.m
static void __SPA_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation)

最后保存lua的方法体到class userdata中

1
2
3
4
5
// SpaClass.m            
lua_getfenv(L, 1);

lua_insert(L, 2);
lua_rawset(L, 2);

最后我们调用的被替换的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// SpaClass.m
static void __SPA_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
	lua_State* L = [[Spa sharedInstace] lua_state];
    spa_safeInLuaStack(L, ^int{
    	// 通过判断是否有对应的ORGI方法,检查是否是被spa替换的
        if (isReplaceBySpa(object_getClass(self), invocation.selector)) {
        	// 关键调用lua方法体的地方
            int nresults = callLuaFunction(L, self, invocation.selector, invocation);
        }
        return 0;
    });
}

static int callLuaFunction(lua_State *L, id self, SEL selector, NSInvocation *invocation) {
	// 获取class userdata
	luaL_getmetatable(L, SPA_CLASS_LIST_TABLE);
    lua_getfield(L, -1, [NSStringFromClass([self class]) UTF8String]);
    lua_getfenv(L, -1);
    
	// 获取lua方法体
    lua_getfield(L, -1, spa_toLuaFuncName(sel_getName(selector)));
	
	// 赋值,这里创建的第一个ViewController的instanceUserData
	[SpaInstance createInstanceUserData:L object:self];
	// 调用执行
	if(lua_pcall(L, nargs, nresults, 0) != 0){
}

方法替换并调用过程总结

• 创建class的userdata, 并且配置元表

• 将patch代码块的环境配置元表,并引导到class userdata的元表中

• class userdata的newindex接收到class、function name、lua function

• 将function name转换成sel,做消息转发,将origin method 添加为另外一个方法

• 将lua function存储到class userdata的环境

• forwardInvocation执行时,从class userdata环境中提取lua function,push到栈 • invocation中根据函数签名提取参数,转换成lua类型,push到栈

• lua_pcall 完成lua function的调用

再看第三行self:view():setBackgroundColor_(UIColor:grayColor())

self就是ViewController的instanceUserdata

1
2
3
4
self:view() --展开就是 

f = self.view
f()

所以走进的是self的这个instanceUserData的__index中

我们回到instanceUserdata创建的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// SpaClass.m callLuaFunction
[SpaInstance createInstanceUserData:L object:self];

// SpaInstance createInstanceUserData:
+ (int)createInstanceUserData:(lua_State *)L object:(id)object {
    return spa_safeInLuaStack(L, ^int{
    	// 获取到instance_list_table
        luaL_getmetatable(L, SPA_INSTANCE_LIST_TABLE);

// 全局搜索SPA_INSTANCE_LIST_TABLE
// SpaInstance.m setup:
- (void)setup:(lua_State *)L {
    luaL_register(L, NULL, MetaMethods);
    luaL_newmetatable(L, SPA_INSTANCE_LIST_TABLE);

static const struct luaL_Reg MetaMethods[] = {
    {"__index", __index},
    {"__newindex", __newIndex},
    {"__call", __call},
    {"__gc", __gc},
    {NULL, NULL}
};

setup又是在什么时候调用的呢,回到

1
2
3
// Spa.m setup:
[self.spa_class setup:L]; // 这一行是不是很熟悉
[self.spa_instance setup:L]; // 这里注入的instance的相关c方法

所以,self.view就走到了SpaInstance的__index方法中 图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SpaInstance __index
lua_pushcclosure(L, spa_invoke, 1); // self:view()

// SpaUtil.m spa_invoke
int spa_invoke(lua_State *L) {
	// 使用invocation执行[self view]方法
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = instance->instance;
    invocation.selector = sel;
	[invocation invoke];
	// 获取返回值,并且创建返回值的instanceUserData
    __unsafe_unretained id object = nil;
    [invocation getReturnValue:&object];
    [SpaConverter toLuaObject:L object:object];
}

// SpaInstance.m createInstanceUserData:
+ (int)createInstanceUserData:(lua_State *)L object:(id)object {
	// 这里又是我们之前讲到的,将返回值的instanceUserData挂SPA_INSTANCE_LIST_TABLE的钩子
}

接着就是执行这个返回值的下一部分setBackgroundColor_(),同上描述,持续调用

备注: 其中UIColor就是走到了我们预先埋入的spa_class lua方法里面的__index去创建一个UIColor的元表(同ViewController元表的创建),然后执行以上方法图片

方法体执行总结

• self也就是instance userdata,调用方法时,通过__index找到selector

• push一个c function到栈

• c function 被lua调用

• c function 通过userdata获取实例,并提取参数转换成OC类型

• NSInvocation完成调用

• 将返回值转换成lua类型,返回给lua

• 持续以上过程完成最终调用

其他

参数持久化原理:是用一个对象持有住参数,然后将对象放到自动释放池中,在对象delloc的时候去执行free

详细见代码:

1
2
3
4
5
6
7
8
9
// SpaConverter.m SpaHoderHelper
struct S* s = malloc(sizeof(struct S));
s->p= p;
__unused SpaHoderHelper* __autoreleasing sh = [[SpaHoderHelper alloc] init:^void{
    free(p);
    free(s);
}];
return s;

参考文章

waxpatch

lua和c的交互原理

lua的理解与介绍

本文由作者按照 CC BY 4.0 进行授权