ios lua 交互

lua文件结构

名称          说明

docLua    相关的文档,包括了编译文档、接口文档等

Makefile   编译Lua使用,在这里我们不使用它来进行编译

README   关于Lua的说明文件

src              Lua的源码文件

注意:在这里我们只需要src目录中的源码文件,先打开src目录,将Makefile、lua.c、luac.c三个文件删除掉,需要说明的是lua.c和luac.c文件是用于编译生成lua和luac两个命令不属于解析器的功能,如果不删除可能会导致XCode无法编译通过。


ios system函数不可用:

1.导入头文件:#include<ftw.h>

2.声明unlink_cb方法:

int unlink_cb(const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf)

{

    int rv = remove(fpath);

    if (rv)

        perror(fpath);

        return rv;

}

3.使用nftw:

nftw(luaL_optstring(L, 1, NULL), unlink_cb, 64, FTW_DEPTH|FTW_PHYS)

lua架构图

1.c api

在开始实现Lua与OC交互之前先来了解两个非常重要的概念,一个是Lua的C Api,Lua解释器是使用C语言来编写的,因此它提供了丰富的C语言定义的接口来访问和操作Lua中的所有元素;

另外一个就是 的概念,在Lua和C进行交互数据的时候会用到了一个栈的结构,栈中的每个元素都能保存任何类型的Lua值。要获取Lua中的一个值时,需要调用一个C Api函数,Lua就会将特定的值压入栈中,然后再通过相应的C Api将值取出来


通过c api获取lua的值的过程

同样,要将一个值传给Lua时,需要先调用C Api将这个值压入栈,然后再调用C Api,Lua就会获取该值并将其从栈中弹出。 如图所示:

通过c api 传递给lua值的的过程

关于栈中位置在lua中有两种形式表示,第一种是正数表示法,1表示栈底元素(即最先入栈的元素),然后越往上的元素,索引值越大。另外一种是负数表示法,-1表示栈顶元素(即最后入栈的元素),然后越往下的元素,索引值越小。如图所示:


stack

1.关于栈操作的C Api

C Api中提供了很多操作栈的功能接口,通常可以分为四大类:入栈操作、查询操作、取值操作和其他操作。

1.1 入栈操作

表示要将本地的某个类型的值放到数据栈中,然后提供给Lua层来获取和操作。

void lua_pushnil (lua_State *L);

void lua_pushnumber (lua_State *L, lua_Number n);

void lua_pushinteger (lua_State *L, lua_Integer n);

const char *lua_pushlstring (lua_State *L, const char *s, size_t len) ;

const char *lua_pushstring (lua_State *L, const char *s);

const char *lua_pushvfstring (lua_State *L, const char *fmt, va_list argp);

const char *lua_pushfstring (lua_State *L, const char *fmt, ...);

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);

void lua_pushboolean (lua_State *L, int b);

void lua_pushlightuserdata (lua_State *L, void *p);

int lua_pushthread (lua_State *L);

从上面的接口方法定义可以看出来,不同的Lua类型对应着不通的入栈接口,包括了整型(Integer)、布尔类型(Boolean)、浮点数(Number)、字符串(String)、闭包(Closure)、用户自定义数据(Userdata)、空类型(Nil)以及线程(Thread)。需要注意的是,C Api没有提供直接入栈Table类型的接口(估计是该数据类型无法与本地结构进行对应),如果需要入栈一个Table类型,可以使用lua_createtable方法来入栈一个Table,调用该方法会在栈顶放入一个Table的引用。

例子:lua_pushinteger (state, 1);  //把1这个值放到栈顶

lua_setglobal (state, "a");  //把栈顶的元素放入该方法第二个参数所指定的变量名对应的lua变量中,同时移除栈顶元素。如图:

lua_setglobal操作

 1.2查询操作

之前说到栈中的每个元素都可以为任意类型,那么,对于如何判断元素的类型就可以通过该类方法来实现。该类方法的定义如下:

int lua_isnil (lua_State *state, int index);

int lua_isboolean (lua_State *state, int index);

int lua_isfunction (lua_State *state, int index);

int lua_istable (lua_State *state, int index);

int lua_islightuserdata (lua_State *state, int index);

int lua_isthread (lua_State *state, int index);

int lua_isnumber (lua_State *L, int idx);

int lua_isinteger (lua_State *L, int idx);

int lua_iscfunction (lua_State *L, int idx);

int lua_isstring (lua_State *L, int idx);

int lua_isuserdata (lua_State *L, int idx)

注意第二个参数index,可以有两种形式;第一种是n位于栈顶,第二种是-1位于栈顶;如下图:

栈index

lua_isXXX系列方主要是判断栈中数据是否能够被转换为对应数据类型时使用,如lua_isstring方法则是判断栈中某个元素是否能够被转换为string类型,所以当栈中数据为number类型时,其返回值也为true。

如果要进行非转换的强类型判断,可以使用lua_type方法来获取栈中元素的类型,然后根据类型来获取值。如判断栈顶元素的类型:

switch(lua_type(state, -1)){

    case LUA_TNUMBER:

        break;

    case LUA_TSTRING:

        break;

     。。。。。。。

      default:

        break;

}

1.3 取值操作

当知道某个数据类型后,则调用对应数据类型的取值方法来获取元素。其方法定义如下:

int lua_toboolean (lua_State *L, int idx);

const char *lua_tolstring (lua_State *L, int idx, size_t *len);

lua_CFunction lua_tocfunction (lua_State *L, int idx);

void *lua_touserdata (lua_State *L, int idx);

lua_State *lua_tothread (lua_State *L, int idx);

const void *lua_topointer (lua_State *L, int idx);

lua_Integer lua_tointegerx (lua_State *L, int idx, int *pisnum);

lua_Number lua_tonumberx (lua_State *L, int idx, int *pisnum);

要注意的是该系列接口跟lua_isXXX系列接口一样,会对原始的类型进行转换输出,因此在做一些跟类型相关的操作时,最好时先判断类型再根据类型调用该方法取值

1.4 其他操作

int lua_gettop (lua_State *L);

void lua_settop (lua_State *L, int idx);

void lua_pushvalue (lua_State *L, int idx);

void lua_remove(lua_State *L, int idx);

void lua_insert(lua_State *L, int idx);

void lua_replace(lua_State *L, int idx);

void lua_pop(lua_State *L, int n);

lua_gettop:获取栈顶位置,也即是栈中元素的个数,其实这个方法在处理原生方法的传入参数时很有用,可以确认传入参数的个数。有时候也可以用它来输出各个状态下的栈元素变化,来确认自己在操作栈时是否存在问题。

lua_settop:用于设置栈顶位置,如果新栈顶高于之前的栈顶则会push一些nil的元素来填充;如果新栈顶低于之前的栈顶则会丢弃新栈顶之上的所有元素。如图所示:


lua_settop

lua_pushvalue:表示将栈中某个元素的副本压入栈顶。之前的栈元素不会发生变动。如图所示:


lua_pushvalue

lua_remove方法用于移除指定索引上的元素,然后再该元素之上的所有元素会下移填补空缺(即元素的索引会发生变更)。如图所示:


lua_remove

lua_insert会将指定索引位置之上的所有元素上移来开辟一个新的位置。然后将栈顶元素插入到该位置。如图所示:


lua_insert

lua_replace方法会先弹出栈顶元素,然后将该元素覆盖到指定索引位置上(栈就减少了少了一个元素)。如图所示:

lua_replace

lua_pop方法会从栈顶弹出指定数量的元素。如图所示:

lua_pop

2. From OC to Lua

2.1 空值传递

使用lua_pushnil方法可以将任意一个Lua变量置空。如:

lua_pushnil();  //推到栈上

lua_setglobal(self.state, "val"); //栈上到c空间

2.2 数值的传递

使用lua_pushinteger或者lua_pushnumber方法来将OC中的数值类型传递到Lua中指定的某个变量。如:

//传递整型值

lua_pushinteger(self.state, 1024);

lua_setglobal(self.state, "intVal");

//传递浮点型

lua_pushnumber(self.state, 80.08);

lua_setglobal(self.state, "numVal");

2.3 布尔值的传递

使用lua_pushboolean方法来实现,如:

lua_pushboolean(self.state, YES);

lua_setglobal(self.state, "boolVal");

2.4 字符串的传递

使用lua_pushstring方法可以传递字符串给Lua,要注意的是该方法接收的是一个c描述的字符串(即 char*)。如:

lua_pushstring(self.state, @"Hello World".UTF8String);

lua_setglobal(self.state, "stringVal");

2.5 二进制数组的传递

二进制数组在Lua中其实与字符串的存储方式相同,但是OC中不能直接使用lua_pushstring来进行二进制数组的传递,可以使用lua_pushlstring方法来传递。如:

char bytes[13] = {0xf1, 0xaa, 0x12, 0x56, 0x00, 0xb2, 0x43, '\0', '\0', 0x00, 0x90, 0x65, 0x73};

lua_pushlstring(self.state, bytes, 13);

lua_setglobal(self.state, "bytesVal");

2.6 方法的传递

Lua中只能接受C定义的方法传入,并且方法的声明必须符合lua_CFunction函数指针的定义,即:

int printHelloWorld (lua_State *state){

    NSLog(@"Hello World!");

    return 0;

}

lua_pushcfunction(self.state, printHelloWorld);

lua_setglobal(self.state, "funcVal");

操作完成后,在Lua中就可以直接调用了:

funcVal();

如果oc的方法是允许接受参数的,那么可以从state参数里面获取传入的参数。拿上面的例子,例如方法接收一个名字的字符串参数,函数的代码则可以修改为:

int printHelloWorld (lua_State*state){

    if(lua_gettop(state)>0){

        const char*name =lua_tostring(state,1); //取值操作会删除栈顶元素

        const char*name2 =lua_tostring(state,2);

        NSLog(@"Hello--%s--%s",name,name2); //

    }

    return 0;

}

lua中调用:funcVal("aaa","bbb")

输出:Hello--aaa--bbb

如果定义的方法不是直接打印字符串,而是组合了字符串给Lua返回,那么定义的方法里面则需要配合’lua_pushXXXX’系列方法来进行返回值传递。需要注意的是:方法中return的数量要与push到栈中的值要一致,否则可能出现异常。那么,上面定义的函数可以做如下修改:

int printHelloWorld (lua_State*state){

    if(lua_gettop(state)>0){

        constchar*name =lua_tostring(state,1);

        NSString*retVal = [NSStringstringWithFormat:@"Hollo %s",name];

        lua_pushstring(state, retVal.UTF8String);

        return 1; //直接返回,因为只有一个return,那么也只能push一次

    }

    return 0;

}

然后在Lua中则可以这样调用:

local retVal = funcVal("vimfung");

print(retVal);

输出:Hollo vimfung

2.7 数组和字典的传递

在Lua中,数组(Array)和字典(Dictionary)都由一个Table类型所表示(在Lua看来数组其实也属于一种字典,只是它的key是有序并且为整数)。如:

-- 定义数组 local arrayVal = {1,2,3,4,5,6};

-- 定义字典 local dictVal = {a=1, b=3, c=4, d=5};

上面的例子分别用了不带key的声明和带key的声明两种方式来创建Table类型。其中不带key的声明方式,解析器会默认为其创建一个key,该key是从1开始,由小到大进行分配,其等效于:

local arrayVal = {1=1, 2=2, 3=3, 4=4, 5=5, 6=6};

当然,两种方式是可以混合使用,如:

local tbl = {1, 2, a=1, b=2, 3};

Table属于比较复杂的数据结构,因此提供操作它的C Api也比较复杂,下面将根据数组和字典分别讲述它们的传递方式。

2.7.1 数组传递

首先,需要将一个Table类型入栈,这样才能对其进行进一步的操作。由于没有pushtable这样的方法,但是可以使用lua_newtable来创建一个Table对象,并且该对象会自动放入栈顶位置。如:

lua_newtable(self.state); //创建一个Table对象并放入栈顶

然后对要传递的数组进行遍历,并通过lua_rawseti方法将元素值设置到Table中。如:

NSArray *array = @[@1, @2, @3, @4, @5, @6];

 [array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

    NSInteger value = [obj integerValue];

    lua_pushinteger(self.state, value); 

    lua_rawseti(self.state, -2, idx + 1); //将当前栈顶元素放入Table中,第二个参数代表Table在栈中的位置,第三个参数代表放入Table的位置

}];

lua_setglobal(self.state, "arrayVal"); 


lua_rawseti


2.7.2 字典传递

字典的传递同样需要先入栈一个Table:

lua_newtable(self.state);

然后对要传递的字典进行遍历,并通过lua_setfield方法将元素设置到Table中。如:

[dict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {

    NSInteger value = [obj integerValue];

    lua_pushinteger(self.state, value);

    lua_setfield(self.state, -2, key.UTF8String);

}];

lua_setglobal(self.state, "dictVal");

lua_setfield与lua_rawseti功能类型,都是把一个元素放入Table中,只是一个用于指定整数索引,一个是指定字符串索引。通过上面的方式就可以把字典传递给Lua了。

2.8 自定义数据传递

Lua中一个比较强大的地方是它可以将任意的类型(包括类对象)进行传递。特别是在提供原生处理方法时,需要用到一些特定的数据类型作为参数时,Lua就可以帮我们实现这一块的传递。

要想传递自定义的数据则必须要使用Lua提供的Userdata类型。该类型有两种引用方式,一种是强引用Userdata,由Lua的GC来负责该类型变量的生命周期。另外一种是弱引用Userdata,又称Light Userdata,该类型不被GC所管理,其生命周期由原生层来决定。下面来看一下两种方式是如何实现的。

首先我们来定义一个OC的User类:

@interface User : NSObject

@property (nonatomic, copy) NSString *name;

@end

利用lua_newuserdata方法来创建一个强引用Userdata,并创建一个User对象赋值给新建的Userdata。如:

void *instanceRef = lua_newuserdata(self.state, sizeof(User *));

instanceRef = (__bridge_retained void *)[[User alloc] init];

lua_setglobal(self.state, "userdataVal");

通过上面的代码就可以把User类实例封装成Userdata再传递给Lua。如果你要传递的对象并不需要Lua来管理生命周期,那么就可以创建一个弱引用的Userdata,如:

User *user = [[User alloc] init];

lua_pushlightuserdata(self.state, (__bridge void *)(user));

lua_setglobal(self.state, "userdataVal");

例子

1.//定义c函数

static int printUser (lua_State *state){

    if (lua_gettop(state) > 0) {

        //表示有参数传入

        User *user = (__bridge User *)(lua_topointer(state, 1));

        NSLog(@"user.name = %@", user.name);

    }

    return 0;

}

2.该方法通过lua_topointer方法来获取了一个Userdata数据类型并转换为User类实例对象然后打印其名称。接下来将其导出给Lua:

lua_pushcfunction(self.state, printUser);

lua_setglobal(self.state, "printUser"); //把printUser函数暴露给lua

3.然后生成一个User对象,并调用该方法传入该用户对象。如:

//创建User对象

User *user = [[User alloc] init];

user.name = @"vimfung";

lua_getglobal(self.state, "printUser"); //把lua中的printUser函数放入栈中

//传入User参数

lua_pushlightuserdata(self.state, (__bridge void *)(user)); //把user对象放入栈中

lua_pcall(self.state, 1, 0, 0); //表示调用栈中的函数,第二个参数为函数参数的数量,第三个是返回值的数量,第四个参数是用于发生错误处理时的代码返回

最终的输出信息如下:user.name = vimfung

3.From Lua to OC

3.1 获取数值

lua_getglobal(self.state, "aa"); //从c空间到栈上

double value = lua_tonumber(self.state, -1); //从栈到oc空间

NSLog(@"aa = %f", value);

lua_pop(self.state, 1);

lua_getglobal方法是用于获取全局变量的值,调用它会把一个值放入栈顶。然后再通过lua_tonumber把栈顶的值读取出来并打印。需要注意的是通过lua_getglobal获取得到的值,在栈中是不会自动清除,因此,在用完某个变量时记得把它从栈中清除掉,代码中是通过lua_pop把值弹出栈的。

3.2 获取布尔值

与获取数值相同,通过lua_toboolean方法来获取某个布尔变量的值。如:

lua_getglobal(self.state, "aa");

BOOL value = lua_toboolean(self.state, -1);

if (value){

  NSLog(@"aa = YES");

}

else{

  NSLog(@"aa = NO");

}

lua_pop(self.state, 1);

3.3 获取字符串

lua_tostring方法来获取字符串变量的值。如:

lua_getglobal(self.state, "aa");

const char *value = lua_tostring(self.state, -1);

NSLog(@"aa = %s", value);

lua_pop(self.state, 1);

3.4 获取二进制数组

lua_tolstring方法来获二进制数组变量的值。如:

lua_getglobal(self.state, "aa");

size_t len = 0;

const char *bytes = lua_tolstring(self.state, -1, &len);

NSData *data = [NSData dataWithBytes:bytes length:len];

NSLog(@"aa = %@", data);

lua_pop(self.state, 1);

3.5 方法的获取和调用

一般情况下,要获取Lua中的某个Function主要是用于对其进行调用。假设有一个Lua方法定义如下:

function printHelloWorld() 

 print("Hello World");

end

那么,OC中则可以进行下面操作来调用方法并传递参数,最终取得返回值然后打印到控制台:

lua_getglobal(self.state, "printHelloWorld");

lua_pcall(self.state, 0, 0, 0);

上述代码中的lua_pcall方法表示将栈中的元素视作Function来进行调用。其中第二个参数为传入参数的数量,必须与压栈的参数数量一致;第三个参数为返回值的数量,表示调用后其放入栈中的返回值有多少个。第四个参数是用于发生错误处理时的代码返回。其运行原理如下图所示:


oc调用lua过程

大概过程是:lua_getglobal先把lua函数入栈,在oc中把要传给lua的参数也入栈;然后lua_pcall将栈中的元素视作Function来进行调用,调用完毕后清空栈,然后将lua函数的返回值放回栈中

举例说明

假设有一个加法的Lua方法,其定义如下:

function add(a, b)

 return a + b;

end

那么,OC中则可以进行下面操作来调用方法并传递参数,最终取得返回值然后打印到控制台:

lua_getglobal(self.state, "add");

lua_pushinteger(self.state, 1000);

lua_pushinteger(self.state, 24);

lua_pcall(self.state, 2, 1, 0);

NSInteger retVal = lua_tonumber(self.state, -1);

NSLog(@"retVal = %ld", retVal);

3.6 Table的获取和遍历

Table的获取跟其他变量一样,一旦放入栈后可以根据需要通过调用lua_getfield方法来指定的key的值。如:

//假设Lua中有一个Table变量aa = {key1=1000, key2=24};

lua_getglobal(self.state, "aa");

lua_getfield(self.state, -1, "key2");

NSInteger value = lua_tonumber(self.state, -1);

NSLog(@"value = %ld", value);

lua_pop(self.state, 1);

如果Table是声明时没有指定key,那么则需要调用lua_rawgeti来获取Table的值。如:

//假设Lua中有一个Table变量aa = {1000, 24};

lua_getglobal(self.state, "aa");

lua_rawgeti(self.state, -1, 2);

NSInteger value = lua_tonumber(self.state, -1);

NSLog(@"value = %ld", value);

lua_pop(self.state, 1);

有时候,Table存储的信息会在函数体外被访问,那么我们需要对Table进行遍历然后把它放入一个字典中,然后提供给程序使用。代码如下:

//假设Lua中有一个Table变量aa = {1000, 24};

lua_getglobal(self.state, "aa");

lua_pushnil(self.state);

while (lua_next(self.state, -2)){

  NSInteger value = lua_tonumber(self.state, -1);

  if (lua_type(self.state, -2) == LUA_TSTRING) {

    const char *key = lua_tostring(self.state, -2);

    NSLog(@"key = %s, value = %ld", key, value);

  }

  else if (lua_type(self.state, -2) == LUA_TNUMBER) {

    NSInteger key = lua_tonumber(self.state, -2);

    NSLog(@"key = %ld, value = %ld", key, value);

  }

  lua_pop(self.state, 1);

}

上述代码利用lua_next方法来遍历Table的所有元素,该方法从栈顶弹出一个元素作为遍历Table的起始Key,然后把每个元素的Key和Value放入栈中。为了遍历所有元素所以起始的Key设置了一个nil值,证明要从Table最开始的Key进行遍历。如图:


值得注意的是,在获取Key值时,最好先判断Key的类型,然后再根据其对应类型调用相应的lua_toXXX方法。否则,因为lua_toXXX系列方法会对元素值进行类型转换,如整型的Key被lua_tostring转换为String后再给到lua_next进行遍历就会报找不到指定Key的错误。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容