深入了解+load方法的执行顺序



从源码层面分析请看这篇文章

+load方法的执行时机

官方文档描述:

Invoked whenever a class or category is added to the Objective-C runtime。The load message is sent to classes and categories that are both dynamically loaded and statically linked, but only if the newly loaded class or category implements a method that can respond.

当一个类或者分类被加载到Objectie-C的Runtime运行环境中时,会调用它对应的+load方法。对于所有静态库中和动态库中实现了+load方法的类和分类都有效。

当应用启动时,首先要fork进程,然后进行动态链接。+load方法的调用就是在动态链接这个阶段进行的。动态链接结束之后,会执行程序的main函数。

dyld简介

dyld(the dynamic link editor)是苹果的动态链接器,是苹果操作系统的一个重要组成部分,在系统内核做好程序准备工作之后,交由dyld负责余下的工作。整个加载过程可细分为九步:

1,设置运行环境

2,记载共享缓存

3,实例化主程序

4,加载插入的动态库

5,链接主程序

6,链接插入的动态库

7,执行弱符号绑定

8,执行初始化方法

9,查找入口点并返回

首先新建一个Pserson类,实现+load方法并打断点,得到下图


从图中可以看出执行顺序是_dyld_start->call_load_methods->Person(+load)

由此可知+load方法会在dyld阶段的执行初始化方法中执行。

官方文档中提到了+load方法的执行顺序

一个类的+load方法调用在它的父类的+load方法之后

一个分类的+load方法调用在它本身类的+load方法之后


接下来继续验证类与类之间的+load方法的执行顺序

@interface Person : NSObject

@end

@implementation Person

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

然后新建一个Student类继承Person类。

@interface Student : Person

@end

@implementation Student

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

再新建一个HighSchoolStudent类继承Student。

@interface HighSchoolStudent : Student

@end

@implementation HighSchoolStudent

+ (void)load

{   

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

开始运行,查看结果:

---- 0x10b04c0c0 +[Person load]

---- 0x10b04c160 +[Student load]

---- 0x10b04c1b0 +[HighSchoolStudent load]

结果和我们猜想的一样,接下来,我们增加一个Animal类

@interface Animal : NSObject

@end

@implementation Animal

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

现在看一下结果

---- 0x1094930e8 +[Person load]

---- 0x109493138 +[Animal load]

---- 0x109493188 +[Student load]

---- 0x1094931d8 +[HighSchoolStudent load]

我们发现,Animal类的+load方法也调用了,但是它的调用顺序,我们还不知道是如何的。这个时候,我们去Build Phases中的Compile Sources中看一下。


我们发现这里面的四个.m顺序与+load方法打印的顺序一致。那么我们把这里的顺序全部调转,然后再看下打印结果。


---- 0x1010331d8 +[Person load]

---- 0x101033138 +[Student load]

---- 0x1010330e8 +[HighSchoolStudent load]

---- 0x101033188 +[Animal load]

我们发现Animal的输出变到最后了,那我们再次修改顺序。

查看打印结果

---- 0x10803e0e8 +[Animal load]

---- 0x10803e1d8 +[Person load]

---- 0x10803e188 +[Student load]

---- 0x10803e138 +[HighSchoolStudent load]

这样我们能得出结论:有继承关系的类的+load方法的执行顺序,是从基类到子类的;没有继承关系的两个类的+load方法的执行顺序是与编译顺序有关的(Build Phases -> Compile Sources中的顺序)。

了解Mach-o文件布局的人应该明白,先编译的类就会在可执行文件的前面,编译顺序也体现到了没有继承关系的两个类的+load方法的执行顺序中了。

类与分类之间+load方法的执行顺序

看完了类与类之间+load方法的执行顺序,我们来看看类与分类,以及分类与分类之间的+load方法的执行顺序。

在刚才例子的基础上,我们在新建Person的两个分类Test1、Test2,以及Student的两分类Test1、Test2,和Animal的分类Test。

@interface Person (Test1)

@end

@implementation Person (Test1)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface Person (Test2)

@end

@implementation Person (Test2)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface Student (Test1)

@end

@implementation Student (Test1)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface Student (Test2)

@end

@implementation Student (Test2)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface Animal (Test)

@end

@implementation Animal (Test)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

运行,看下执行结果

---- 0x10b1fd3d8 +[Animal load]

---- 0x10b1fd4c8 +[Person load]

---- 0x10b1fd478 +[Student load]

---- 0x10b1fd428 +[HighSchoolStudent load]

---- 0x10b1fd3d8 +[Animal(Test) load]

---- 0x10b1fd478 +[Student(Test2) load]

---- 0x10b1fd478 +[Student(Test1) load]

---- 0x10b1fd4c8 +[Person(Test1) load]

---- 0x10b1fd4c8 +[Person(Test2) load]

有了上面的经验,我们来看下现在的Complie Sources里面的顺序。


到现在为止我们能确定的是,所有分类的+load方法都要在所有类的+load方法之后执行。然后我们修改一些顺序。


再来看看执行结果。

---- 0x108ff1478 +[Person load]

---- 0x108ff14c8 +[Student load]

---- 0x108ff13d8 +[HighSchoolStudent load]

---- 0x108ff1428 +[Animal load]

---- 0x108ff1478 +[Person(Test2) load]

---- 0x108ff14c8 +[Student(Test2) load]

---- 0x108ff14c8 +[Student(Test1) load]

---- 0x108ff1478 +[Person(Test1) load]

---- 0x108ff1428 +[Animal(Test) load]

经过两次的对比我们发现,之前我们猜测正确:所有分类的+load方法都在所有类+load方法之后执行,同时又发现所有分类的+load方法的执行顺序与编译顺序有关,与是谁的分类无关,也与一个类有几个分类无关。

接着上面咱们刚刚说的dyld的执行初始化方法继续说,在Runtime的源码中,可以看到call_load_methods方法的实现。

void call_load_methods(void)

{

    static bool loading = NO;

    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.

    if (loading) return;

    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {

        // 1. Repeatedly call class +loads until there aren't any more

        while (loadable_classes_used > 0) {

            call_class_loads();

        }

        // 2. Call category +loads ONCE

        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories

    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;

}

从这里我们从代码及注释中也能看到:

1,循环调用call_class_loads方法,直到没有可执行的+load方法

2,调用call_category_loads方法

3,重复1->2,直到所有的类和分类的+load方法都执行完毕

所以在这里也能看出来,所有的类的+load方法都执行在分类的+load方法之前。

我们再来看看call_class_loads源码。

static void call_class_loads(void)

{

    int i;


    // Detach current loadable list.

    struct loadable_class *classes = loadable_classes;

    int used = loadable_classes_used;

    loadable_classes = nil;

    loadable_classes_allocated = 0;

    loadable_classes_used = 0;


    // Call all +loads for the detached list.

    for (i = 0; i < used; i++) {

        Class cls = classes[i].cls;

        load_method_t load_method = (load_method_t)classes[i].method;

        if (!cls) continue;

        if (PrintLoading) {

            _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());

        }

        (*load_method)(cls, SEL_load);

    }


    // Destroy the detached list.

    if (classes) free(classes);

}

代码循环的次数是loadable_classes_used,这个变量在add_class_to_loadable_list方法中每添加一个Class对象,计数加一。所以在执行到这里的时候,就是当前所有已经加载好的Class对象的数量。loadable_classes数组也是在这个方法中一个一个把Class加进去的。所以无关的两个Class的执行顺序,与编译顺序有关。循环中得到load_method后,调用(*load_method)(cls, SEL_load)方法来调用+load方法。

接下来,再看一下call_category_loads方法

static bool call_category_loads(void)

{

    int i, shift;

    bool new_categories_added = NO;


    // Detach current loadable list.

    struct loadable_category *cats = loadable_categories;

    int used = loadable_categories_used;

    int allocated = loadable_categories_allocated;

    loadable_categories = nil;

    loadable_categories_allocated = 0;

    loadable_categories_used = 0;

    // Call all +loads for the detached list.

    for (i = 0; i < used; i++) {

        Category cat = cats[i].cat;

        load_method_t load_method = (load_method_t)cats[i].method;

        Class cls;

        if (!cat) continue;

        cls = _category_getClass(cat);

        if (cls  &&  cls->isLoadable()) {

            if (PrintLoading) {

                _objc_inform("LOAD: +[%s(%s) load]\n",

                            cls->nameForLogging(),

                            _category_getName(cat));

            }

            (*load_method)(cls, SEL_load);

            cats[i].cat = nil;

        }

    }

    // Compact detached list (order-preserving)

    shift = 0;

    for (i = 0; i < used; i++) {

        if (cats[i].cat) {

            cats[i-shift] = cats[i];

        } else {

            shift++;

        }

    }

    used -= shift;

    // Copy any new +load candidates from the new list to the detached list.

    new_categories_added = (loadable_categories_used > 0);

    for (i = 0; i < loadable_categories_used; i++) {

        if (used == allocated) {

            allocated = allocated*2 + 16;

            cats = (struct loadable_category *)

                realloc(cats, allocated *

                                  sizeof(struct loadable_category));

        }

        cats[used++] = loadable_categories[i];

    }

    // Destroy the new list.

    if (loadable_categories) free(loadable_categories);

    // Reattach the (now augmented) detached list.

    // But if there's nothing left to load, destroy the list.

    if (used) {

        loadable_categories = cats;

        loadable_categories_used = used;

        loadable_categories_allocated = allocated;

    } else {

        if (cats) free(cats);

        loadable_categories = nil;

        loadable_categories_used = 0;

        loadable_categories_allocated = 0;

    }

    if (PrintLoading) {

        if (loadable_categories_used != 0) {

            _objc_inform("LOAD: %d categories still waiting for +load\n",

                        loadable_categories_used);

        }

    }

    return new_categories_added;

}

基本上与load_class_loads方法类似,同时还做了一些其他操作。在这里看,我们也就能了解,该函数会获取到所有类及分类的+load方法并执行,所以我们不必手动调用[super load]方法,也能执行到父类的+load方法。

多个镜像中存在+load方法的执行顺序

我们都知道iOS应用的可执行文件,最后会作为一个镜像,加载到内存中,那如果我们还包含动态库和静态库呢?其实静态库会与我们的主程序编译在同一个可执行文件中,也就是一个镜像。但是即便他们在同一个镜像中,主程序与静态库都存在+load方法,其执行顺序是如何的呢?那与主程序不在同一镜像中的动态库中的+load方法,其执行顺序又是如何的呢?

我们在上面的Demo工程中,新建三个Target分别是Cocoa Touch Static Library,以及两个Cocoa Touch Framework,其中两个Framework,设定Mach-o Type一个是Static Library,一个是Dynamic Library,Target名称分别为TestStaticLib、TestStaticFramework和TestDynamcFramework,三个Target中分别有一个类和对应的分类,的代码如下

@interface TestStaticLib : NSObject

@end

@implementation TestStaticLib

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface TestStaticLib (Test)

@end

@implementation TestStaticLib (Test)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface TestStaticFramework : NSObject

@end

@implementation TestStaticFramework

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface TestStaticFramework (Test)

@end

@implementation TestStaticFramework (Test)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface TestDynamicFramework : NSObject

@end

@implementation TestDynamicFramework

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

@interface TestDynamicFramework (Test)

@end

@implementation TestDynamicFramework (Test)

+ (void)load

{

    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

执行观察结果

---- 0x1072ab1e8 +[TestDynamicFramework load]

---- 0x1072ab1e8 +[TestDynamicFramework(Test) load]

---- 0x106fb8478 +[Person load]

---- 0x106fb84c8 +[Student load]

---- 0x106fb83d8 +[HighSchoolStudent load]

---- 0x106fb8428 +[Animal load]

---- 0x106fb8478 +[Person(Test2) load]

---- 0x106fb84c8 +[Student(Test2) load]

---- 0x106fb84c8 +[Student(Test1) load]

---- 0x106fb8478 +[Person(Test1) load]

---- 0x106fb8428 +[Animal(Test) load]

首先输出的动态库中的类的+load方法及子类的+load方法,后面的是主工程的输出,我们之前已经看过的。

到这里我们不难发现,动态库由于与主工程不是同一个镜像,所以他们之间的输出是分开的,而且动态库的链接要优先与主工程的链接,来保证主工程链接时能链接到期望的动态库。所以动态库的+load方法都要在主工程的+load方法之前执行。其中动态库中类与子类、类与类之间的+load方法的执行顺序,与之前说的一致,这里就不再赘述。

但是我们还发现一个问题,静态库.a和.framework都没有打印结果。原因,我们也能想到,因为我们没有调用到这两个库的代码,所以也就没有把这两个库加载,链接进来。所以我们只需要在主工程代码中调用一下这两个库中的类即可。

#import "ViewController.h"

#import "TestStaticLib.h"

#import "TestStaticFramework.h"

@implementation ViewController

- (void)viewDidLoad {

    [super viewDidLoad];

    // Do any additional setup after loading the view, typically from a nib.

    [[TestStaticLib alloc] init];

    [[TestStaticFramework alloc] init];

}

@end

看下打印结果

---- 0x1060a1198 +[TestDynamicFramework load]

---- 0x1060a1198 +[TestDynamicFramework(Test) load]

---- 0x105dae518 +[Person load]

---- 0x105dae568 +[Student load]

---- 0x105dae478 +[HighSchoolStudent load]

---- 0x105dae4c8 +[Animal load]

---- 0x105dae5b8 +[TestStaticLib load]

---- 0x105dae608 +[TestStaticFramework load]

---- 0x105dae518 +[Person(Test2) load]

---- 0x105dae568 +[Student(Test2) load]

---- 0x105dae568 +[Student(Test1) load]

---- 0x105dae518 +[Person(Test1) load]

---- 0x105dae4c8 +[Animal(Test) load]

我们看到了连个静态库类的+load方法打印,在主程序的类的+load方法之后,在主程序的分类的+load方法之前。我们再在Buld Phases -> Link Binary With Libraries中修改一下两个静态库的先后顺序。

---- 0x1060a1198 +[TestDynamicFramework load]

---- 0x1060a1198 +[TestDynamicFramework(Test) load]

---- 0x105dae518 +[Person load]

---- 0x105dae568 +[Student load]

---- 0x105dae478 +[HighSchoolStudent load]

---- 0x105dae4c8 +[Animal load]

---- 0x105dae608 +[TestStaticFramework load]

---- 0x105dae5b8 +[TestStaticLib load]

---- 0x105dae518 +[Person(Test2) load]

---- 0x105dae568 +[Student(Test2) load]

---- 0x105dae568 +[Student(Test1) load]

---- 0x105dae518 +[Person(Test1) load]

---- 0x105dae4c8 +[Animal(Test) load]

我们发现,静态库中的类的+load方法,是必须要有代码调用才能加载链接,并且其类的+load方法的执行顺序与编译顺序有关(Link Binary With Libraries的顺序)。

但是这里还有一个问题,静态库中的分类的+load方法没有调用,其实经常使用静态库开发的同学就知道了,要在主工程的other linker flag中设置-all_load,设置完毕查看运行结果。

---- 0x108a68198 +[TestDynamicFramework load]

---- 0x108a68198 +[TestDynamicFramework(Test) load]

---- 0x108775608 +[Person load]

---- 0x108775658 +[Student load]

---- 0x108775568 +[HighSchoolStudent load]

---- 0x1087755b8 +[Animal load]

---- 0x1087756a8 +[TestStaticFramework load]

---- 0x1087756f8 +[TestStaticLib load]

---- 0x108775608 +[Person(Test2) load]

---- 0x108775658 +[Student(Test2) load]

---- 0x108775658 +[Student(Test1) load]

---- 0x108775608 +[Person(Test1) load]

---- 0x1087755b8 +[Animal(Test) load]

---- 0x1087756a8 +[TestStaticFramework(Test) load]

---- 0x1087756f8 +[TestStaticLib(Test) load]

看静态库中的分类的+load方法调用了,而且打印顺序与静态库中的类的+load方法的打印顺序一致。

如果在+load方法中调用[super load]会有什么影响

我们就继续看例子吧,还是在demo中Student的主类中的+load方法中,调用[super load]。

@implementation Student

+ (void)load

{

    [super load];


    NSLog(@"---- %p %s", self, __FUNCTION__);

}

@end

查看打印结果

---- 0x10a7b4198 +[TestDynamicFramework load]

---- 0x10a7b4198 +[TestDynamicFramework(Test) load]

---- 0x10a4c1618 +[Person load]

---- 0x10a4c1668 +[Person(Test1) load]

---- 0x10a4c1668 +[Student load]

---- 0x10a4c1578 +[HighSchoolStudent load]

---- 0x10a4c15c8 +[Animal load]

---- 0x10a4c16b8 +[TestStaticFramework load]

---- 0x10a4c1708 +[TestStaticLib load]

---- 0x10a4c1618 +[Person(Test2) load]

---- 0x10a4c1668 +[Student(Test2) load]

---- 0x10a4c1668 +[Student(Test1) load]

---- 0x10a4c1618 +[Person(Test1) load]

---- 0x10a4c15c8 +[Animal(Test) load]

---- 0x10a4c16b8 +[TestStaticFramework(Test) load]

---- 0x10a4c1708 +[TestStaticLib(Test) load]

我们发现第四行调用了[Person(Test1) load]方法,而且在后面这个方法还继续调用了一次。这个原因是什么呢?

首先我们在之前得到的结论,在执行到Student的+load方法之前,其父类Person的+load方法已经完毕了。此时我们执行Student的+load方法,调用了[super load],将父类的+load方法再次执行一次。那么这里为什么是[Person(Test1) load]呢,我们看一下编译顺序。


我们知道分类如果与类方法重名了,那么在之后调用时,会调用分类的同名方法,如果多个分类都实现了这个方法,那么就会按照编译顺序,最后执行最后编译的分类中的同名方法,于是就有了这样的结果。在后面,执行到分类的+load方法时,会把该方法再次执行一次。

所以为了避免一些不必要的麻烦,我们就不必手动去写[super load]方法,同时也不要自己手动调用[object load]方法。

总结

结合了例子以及dyld、Runtime的源码,弄清楚了+load方法的执行时机,以及顺序。下面就是一些总结

1,+load方法是在dyld阶段的执行初始化方法步骤中执行的,其调用为load_images -> call_load_methods

2,一个类在代码中不主动调用+load方法的情况下,其类、子类实现的+load方法都会分别执行一次

3,父类的+load方法执行在前,子类的+load方法在后

4,在同一镜像中,所有类的+load方法执行在前,所有分类的+load方法执行在后

5,同一镜像中,没有关系的两个类的执行顺序与编译顺序有关(Compile Sources中的顺序)

6,同一镜像中所有的分类的+load方法的执行顺序与编译顺序有关(Compile Sources中的顺序),与是谁的分类,同一个类有几个分类无关

7,同一镜像中主工程的+load方法执行在前,静态库的+load方法执行在后。有多个静态库时,静态库之间的执行顺序与编译顺序有关(Link Binary With Libraries中的顺序)

8,不同镜像中,动态库的+load方法执行在前,主工程的+load执行在后,多个动态库的+load方法的执行顺序编译顺序有关(Link Binary With Libraries中的顺序)。

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

推荐阅读更多精彩内容