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
读完本篇章之后,希望你对下面三个问题能有点头绪。
- vc替换doSomeThing这件事,lua怎么传递给OC那边的?
- 当OC在执行doSomeThing的时候,如何执行到lua实现的版本的?
- lua调用函数的时候,又如何转换成调用OC的函数?
lua特性介绍
安装并打开zerobrane,第一步先来熟悉下lua的比较重要的两个特性,元表和环境
setmetatable 元表
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
- 当get表中不存在某个key的时候,会转移到设置的元表中__index所指向的地方
- 当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;