Swift闭包

前情提要

Swift的闭包和OC的Block是一回事,是一种特殊的函数-带有自动变量的匿名函数。

分别从语法和原理上讲解闭包

语法

Swift中,函数和闭包都是引用类型

无论你什么时候赋值一个函数或者闭包给常量或者变量,你实际上都是将常量和变量设置为对函数和闭包的引用

如果你分配了一个闭包给类型的属性,并且闭包通过引用该实例或者它的成员来捕获实例,你将在闭包和实例间产生循环引用

闭包表达式:

闭包表达式语法能够使用常量形式参数,变量形式参数,输入输出形式参数,以及可变形式参数,但在使用可变形式参数时需要在形式参数列表的最后面使用。参数不能提供默认值。元组也可被用来作为形式参数和返回类型

{    (parameter) -> (return type) in

    statements

}

从sorted(by:)函数说起,它会根据你提供的排序闭包将已知类型的数组的值进行排序。一旦完成,sorted方法会返回排序好的新数组并且不会改变原数组。

不用闭包:

let names = ["zhangsan", "lisi", "wang2", "liu5"]

func backward(_ s1: String, _ s2: String) -> Bool {

    return s1 > s2

}

var reversedNames = names.sorted(by: backward)

print(reversedNames)

使用闭包:

let names = ["zhangsan", "lisi", "wang2", "liu5"]

let reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in 

    return s1 > s2

 })

print(reversedNames)

因为排序闭包作为实际参数来传递给函数,所以Swift能推断出它的形式参数类型和返回值类型

sorted(by:)方法期望它的形式参数是一个(String, String) -> Bool类型的函数。这意味着(String, String)和Bool类型不需要被写成闭包表达式定义中的一部分,因为所有的类型都能被推断,返回箭头(->)和围绕在形式参数名周围的括号也能被省略。

let names = ["zhangsan", "lisi", "wang2", "liu5"]

let reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })

print(reversedNames)

单表达式的闭包能够通过从它们的声明中删掉return关键字来隐式返回它们单个表达式的结果(函数也可以,重点是单表达式)

let names = ["zhangsan", "lisi", "wang2", "liu5"]

let reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })

print(reversedNames)

Swift自动对行内闭包提供简写实际参数名,可以通过$0,$1,$2等名字来引用闭包的实际参数值。

let names = ["zhangsan", "lisi", "wang2", "liu5"]

let reversedNames = names.sorted(by: {  $0 > $1 })

print(reversedNames)

Swift的String类型定义了关于大于号(>)的特定字符串实现,让其作为一个有两个String类型形式参数的函数并返回一个Bool类型的值。这正好与sorted(by:)方法相匹配。因此,你能简单地传递一个大于号,并且Swift将推断你想使用大于号特殊字符串函数实现。

let names = ["zhangsan", "lisi", "wang2", "liu5"]

let reversedNames = names.sorted(by: > )

print(reversedNames)



尾随闭包

如果你需要将一个很长的闭包表达式作为函数最后一个实际参数传递给函数,使用尾随闭包将增强函数的可读性。尾随闭包是一个被书写在函数形式参数的括号外面的闭包表达式,例:XXX(_ xx: X, ...)尾随闭包

let names = ["zhangsan", "lisi", "wang2", "liu5"]

let reversedNames = names.sorted{  $0 > $1 }

print(reversedNames)

捕获值

一个闭包能够从上下文捕获已被定义的常量和变量。即使定义这些常量和变量的原作用于已经不存在,闭包仍能够在其函数体内引用和修改这些值

作为一种优化,如果一个值没有改变或者在闭包的外面(闭包没用它),Swift不会去捕获它

Swift也处理了变量的内存管理操作,当变量不再需要时会被释放

func makeIncrementer(forIncrement amount: Int) -> () -> Int {

    var runningTotal = 0

    func incrementer() -> Int {

// 捕获了runnigTotal 并在这个闭包(内嵌函数)内,申请了一块新的内存空间给变量runningTotal

        runningTotal += amount

        retun runningTotal

    }

    return incrementer

}

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen() --10

incrementByTen() --20

incrementByTen() --30

如果你有一个新的变量引用了函数makeIncrementer

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen() --10

incrementByTen() --20

incrementByTen() --30

let incrementBySeven =makeIncrementer(forIncrement:7)

let incrementBySeven() --7

let incrementBySeven() --14

let incrementBySeven() --21

通过内存地址来分析这一过程都发生了什么?

分析代码:

fileprivate func makeIncrementer(forIncrement amount:Int) -> () ->Int{

        var runningTotal =0

        print("-------------------")

        print("内嵌函数外的runningTotal地址:")

        print(Unmanaged.passUnretained(runningTotalasAnyObject).toOpaque())

        print("-------------------")

        func incrementer() ->Int{

    // 捕获了runnigTotal 并在这个闭包(内嵌函数)内,申请了一块新的内存空间给变量runningTotal

            runningTotal+=amount

            print("内嵌函数内的runningTotal地址:")

            print(Unmanaged.passUnretained(runningTotalasAnyObject).toOpaque())


            return runningTotal

        }

        return incrementer

    }



       leti ncrementByTen =makeIncrementer(forIncrement:10)

        print(incrementByTen())

        print(incrementByTen())

        print(incrementByTen())

        let incrementBySeven =makeIncrementer(forIncrement:7)

        print(incrementBySeven())

        print(incrementBySeven())

        print(incrementBySeven())


        print(makeIncrementer(forIncrement:10)())

        print(makeIncrementer(forIncrement:10)())

        print(makeIncrementer(forIncrement:11)())

代码对应的print:

-------------------

内嵌函数外的runningTotal地址:

0xbd27871c8d0870d3

-------------------

内嵌函数内的runningTotal地址:

0xbd27871c8d08707310

内嵌函数内的runningTotal地址:

0xbd27871c8d08719320

内嵌函数内的runningTotal地址:

0xbd27871c8d08713330

-------------------

内嵌函数外的runningTotal地址:

0xbd27871c8d0870d3

-------------------

内嵌函数内的runningTotal地址:

0xbd27871c8d0870a37

内嵌函数内的runningTotal地址:

0xbd27871c8d08703314

内嵌函数内的runningTotal地址:

0xbd27871c8d08718321

-------------------

内嵌函数外的runningTotal地址:

0xbd27871c8d0870d3

-------------------

内嵌函数内的runningTotal地址:

0xbd27871c8d08707310

-------------------

内嵌函数外的runningTotal地址:

0xbd27871c8d0870d3

-------------------

内嵌函数内的runningTotal地址:

0xbd27871c8d08707310

-------------------

内嵌函数外的runningTotal地址:

0xbd27871c8d0870d3

-------------------

内嵌函数内的runningTotal地址:

0xbd27871c8d08706311

分析

1.变量incrementByTen和incrementBySeven引用的函数makeIncrementer是相同的,makeIncrementer(forIncrement:10)和makeIncrementer(forIncrement:7)所返回的内嵌函数(闭包)地址也是相同的,并没有创建两个blcok;

2.在执行赋值(let incrementByTen = makeIncrementer(forIncrement:10))的过程中,内嵌函数外的runningTotal地址也是相同的;

3.在执行incrementByTen()时,内嵌函数内的runningTotal地址每一次都是不同的,意味着runningtotal被blcok所捕获,每执行一次都会在前一次的基础上把runningtotal复制到一个新地址上再执行相加。

4.累加,是因为变量incrementByTen/incrementBySeven维护着自己block捕获的变量。当单独执行makeIncrementer(forIncrement:10)()时,无论多少次,也不会累加,因为没有变量去维护block捕获的变量,执行完就释放了。

5.第一次执行incrementByTen()和直接执行makeIncrementer(forIncrement:10)(),的内嵌函数runningTotal地址一样,这是内存行为,提高了效率。

逃逸闭包

当闭包作为一个实际参数传递给一个函数的时候,并且它会在函数返回之后调用(比如封装的网络请求),我们就说这个闭包逃逸了。当你声明一个接受闭包作为形式参数的函数时(比如封装的带闭包的网络请求),你可以在形式参数前写@escaping来明确闭包是允许逃逸的。

闭包可以逃逸的一种方法是被存储在定义于函数外的变量里。比如说,很多函数接收闭包实际参数来作为启动异步任务的回调。函数在启动任务后返回,但是闭包要直到任务完成--闭包需要逃逸,以便稍后调用

让闭包@escaping意味着你必须在闭包中显示地引用self

var completionHandlers: [() -> void] = []

func someFunctionWithEscapingClousure(completionHandler: @escaping () -> void) {

    completionHandlers.append(completionHandler)

}

func someFuncWithNonEscapingClosure(closure: () -> void) {

    closure()

}

class SomeClass {

    var x = 10

    func doSomething() {

        someFunctionWithEscapingClousure {self.x = 100}

         someFuncWithNonEscapingClosure {x = 200}

    }

}

let instance = someClass()

instance.doSomething()

print(instance.x) -- 200

completionHandlers.first?()

print(instance.x) -- 100

自动闭包

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。

这个语法的好处在于通过写普通表达式代替显示闭包而使你省略包围函数形式参数的括号

public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String(), file: StaticsString = #file, line: Uint = #line)

let number = 3

assert(number > 3, "number 不大于3")

自动闭包允许你延迟处理,因此闭包内部的代码直到你调用它的时候才会运行。对于有副作用或者占用资源的代码来说很有用,因为它可以允许你控制代码何时才进行求值

var customersInLine = ["熬", "2", "3", "4", "5"]

print(customersInLine.count) -- 5

let customerProvider = { customersInLine.remove(at: 0) }

print(customersInLine.count) -- 5

print("Now serving \(customerProvider())!") -- 熬

print(customersInLine.count) -- 4

当你传一个闭包作为实际参数到函数的时候,你会得到与延迟处理相同的行为

fileprivate func serve(customer customerProvider: () ->String) {

        print("Now serving \(customerProvider())!")

    }

serve(customer: {customersInLine.remove(at: 0)}) -- Now serving 熬!

通过@autoclosure标志标记他的形式参数使用了自动闭包。现在你可以调用函数就像它接收了一个String实际参数而不是闭包。实际参数自动地转换为闭包,因为customerProvider形式参数的类型被标记为@autoclosure。

fileprivate func serve(customer customerProvider: @autoclosure () ->String) {

        print("Now serving \(customerProvider())!")

    }

serve(customer: customersInLine.remove(at: 0)) --Now serving 熬!

同时使用逃逸闭包和自动闭包

        func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String)             {

                customerProvides.append(customerProvider)

            }

        var customerProvides: [() -> String] = []

        var customersInLine = ["熬","2","3","4","5"]

        collectCustomerProviders(customersInLine.remove(at:0))

        collectCustomerProviders(customersInLine.remove(at:0))

        print("Collected\(customerProvides.count)closures.")

        for customerProvider in customerProvides{

            print("Now serving \(customerProvider())!")

        }

        print("Remainning\(customerProvides.count)closures.")



原理

闭包/Block是一个,带有自动变量值(可以截获自动变量值)的匿名函数;

截获的含义是保存改自动变量的瞬间值

OC中如果要改变block截获的外部自动变量的值需要在改值前加上__block修饰符,Swift不用,系统会自动处理这些问题。延伸到对象,对于Swift来说系统也会处理但OC不同。

-OC下,截获一个对象(NSMutableArray)mArray,在block内调用NSMutableArray的方法(addObject:)没问题,因为Block捕获的是NSMutableArray类对象用的结构体实例指针,但对其进行操作,比如赋值,就会产生编译错误,要在变量前加上__block。Swift下直接用就行了。

从C代码的角度分析closure/block。OC可以直接用 clang -rewrite-objc 原文件名 来分析,swift下的clang我没有找到对应的命令,所以只看oc的。在分析之前,先介绍两个小概念。

1.OC&Swift中的self

OC Code:

- (void) method:(int)arg {

    NSLog(@"%p %d\n", self, arg);

}

MyObject *obj = [[MyObject alloc] init];

[obj method: 10];

转换成C Code:

void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg) {

    NSLog(@"%p %d\n", self, arg);

}

MyObject *obj = objc_msgSend(objc_getClass("MyObject"), sel_registerName("alloc"));

obj = objc_msgSend(obj, sel_registerName("init"));

objc_msgSend(obj, sel_registerName("method:"), 10);

最后一条执行语句,也就是[obj method: 10] -> objc_msgSend(obj, sel_registerName("method:"), 10)。objc_msgSend(obj, sel_registerName("method:"), 10)函数根据指定的对象和函数名,从对象持有类(MyObject)的结构体中检索method:10对应的函数void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的指针并调用。此时,objc_msgSend函数的第一个参数obj作为_I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的第一个参数self进行传递。即self就是Myobject类的对象-obj自己。



2.OC中的id

id在C语言中的声明:

typedef struct objc_object {

    Class isa;

} *id;

-id为objc_object结构体的指针类型。

Class在C语言中的声明:

typedef struct objc_class {

    Class isa;

} *Class;

-Class为objc_class结构体的指针类型。

这与objc_object结构体相同。然而,objc_object结构体和objc_class结构体归根结底是在各个对象(id)和类(Class)的实现中使用的最基本的结构体。

例:

类MyObject

@interface MyObject : NSObject {

    int val0;

    int val1;

}

@end

他是结构体,一个基于objc_class结构体的class_t结构体。

struct class_t {

    struct class_t *isa;

    struct class_t *superclass;

    Cache cache;

    IMP *vtable;

    uintptr_t data_NEVER_USE;

};

而基于MyObject创建的对象(MyObject1/2/3/4/5)的结构体:

struct MyObject1/2/3/4/5 {

    Class isa;

    int val0;

    int val1;

}

MyObject类的实例变量val0,val1被直接声明为对象的结构体成员。OC中由类生成对象就意味着,生成一个基于该类的结构体实例,这些结构体实例(对象)通过isa保持该类的结构体实例指针(这个例子就是*class_t)。举个例子就是:有3个类,C动物->C狗->C柯基,基于柯基生成的一个对象叫做KK,那么KK这个结构体就会有一个isa指向C柯基,而这个C柯基类的结构体中还会有一个isa指向C狗,同理C狗的isa指向C动物,一层一层地指向父类(super),直到Class,而Class中的isa指向的就是自己,也就到头了。

class_t中持有,声明的成员变量,方法的名称,方法的实现(函数指针),属性以及父类的指针,并被OC运行时库所使用的。



介绍完两个小概念,开始从C代码的角度分析Block:

.m文件下的代码

int main() {

    void(^blk)(void) = ^{printf("Block\n");};

    blk();

    return0;

}



clang -rewrite-objc 项目文件.m之后,摘取有用的内容:

```c

struct __block_impl {

  void*isa;

  intFlags;

  intReserved;

  void*FuncPtr;

};

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {

    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

  }

};

static void__main_block_func_0(struct__main_block_impl_0 *__cself) {printf("Block\n");}

static struct__main_block_desc_0 {

  size_t reserved;

  size_t Block_size;

}

__main_block_desc_0_DATA= {0, sizeof(struct __main_block_impl_0)};

int main() {

    void(*blk)(void) = ((void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA));

    ((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return0;

}

```

粗略来看大概有5部分,一步一步看;

1.源码中的^{printf("Block\n");};转换后的代码是static void__main_block_func_0(struct __main_block_impl_0 *__cself) {

printf("Block\n");}

通过转换后的代码可知,通过Block使用的匿名函数实际上被作为简单的C语言函数来处理。

另外根据Block语法所属的函数名(此处为main)和该Block语法在该函数出现的顺序值(此处为0)来给经clang变换的函数命名(void__main_block_func_0)。

该函数的参数*__cself就是指向Block值的变量,就是他自己。相当于OC实例方法中指向对象自身的变量self。上面的第一个小概念有详细解释。来看看该参数,__cself是__main_block_impl_0结构体的指针。

那我们来看看__main_block_impl_0结构体,去掉构造函数后:

struct __main_block_impl_0 {

  struct __block_impl impl;

  struct __main_block_desc_0* Desc;

};

第一个成员变量是impl,他的结构体声明是:

struct__block_impl {

  void *isa;

  int Flags;

  int Reserved;

  void *FuncPtr;

};

impl是虚函数列表,该结构体内存放了一个标志,包括今后版本升级所需的区域及函数指针。

第二个成员变量是Desc指针,他的结构体的声明是:

static struct__main_block_desc_0 {

  size_t reserved;

  size_t Block_size;

}__main_block_desc_0_DATA= {0,sizeof(struct__main_block_impl_0)};

这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block大小。

这两个成员变量看完之后,再来看一下初始化含有这些结构体的__main_block_impl_0结构体的构造函数,就是我们刚才忽略的那一部分代码,这个代码执行的完成意味着_cself初始化完成:

__main_block_impl_0(void *fp,struct__main_block_desc_0 *desc,intflags=0) {

    impl.isa = &_NSConcreteStackBlock;

    impl.Flags = flags;

    impl.FuncPtr = fp;

    Desc = desc;

  }

在分析这个结构体之前,先看一下这个构造函数的调用(在 int main() {...}中):

void(*blk)(void) = ((void(*)(void))&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA));

去掉转换部分 简化成两行代码:

struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

struct __main_block_impl_0 *blk = &tmp;

该源代码将__main_block_impl_0结构体类型的自动变量(栈上生成的__main_block_impl_0结构体实例的指针),赋值给__main_block_impl_0结构体指针类型的变量blk,对应未转换前的代码就是这个赋值操作:

void(^blk)(void) = ^{printf("Block\n");};

即将Block语法生成的Block赋给Block类型变量blk。该源代码中的Block就是__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例。然后指针赋值。

接下来看看__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);结构体实例构造参数。__main_block_func_0和&__main_block_desc_0_DATA。

__main_block_func_0是一个由Block语法转换的C语言函数指针;

&__main_block_desc_0_DATA是作为静态全局变量初始化的__main_block_desc_0结构体实例指针。其初始化代码如下:

__main_block_desc_0_DATA= {0,sizeof(struct__main_block_impl_0)};

由此可知,改源代码使用Block,即__main_block_impl_0结构体实例的大小,进行初始化。

下面综合来看一下,栈上的Block即__main_block_impl_0结构体实例的初始化是怎样完成的(展开):

struct__main_block_impl_0 {

    void *isa;

    Int Flags;

    int Reserved;

    void &FuncPtr;

    struct __main_block_desc_0* Desc;

}

该结构体会根据构造函数进行如下的初始化:

isa = &_NSConcreteStackBlock;

Flags = 0;

Reserved = 0;

FuncPtr = __main_block_func_0;

Desc = &__main_block_desc_0_DATA;

即,将__main_block_func_0函数指针赋值给成员变量FunPtr。



源码中使用blk的部分blk()对应转换后的代码是:

((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉转换部分:

(*blk->impl.FuncPtr)(blk);

这就是简单地使用函数指针调用函数。正如我们刚才所确认的,有Block语法转换的__main_block_func_0函数的指针被赋值成员变量FuncPtr中。另外也说明了,__main_block_func_0函数的参数__cself指向Block值。在调用改函数的源代码中可以看出Block正是作为参数进行了传递。

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