iOS里的内存管理

前言

什么是内存管理?是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

我们本篇学习的就是iOS开发中是如何对内存进行管理的。其中有部分章节是从前人的文章中搬运过来整理而成,这些文章里已经对部分知识点解释的很清楚明了了,我也没有更好的表达方式,所以站在巨人的肩膀上,我只是一个整理者加了部分自己的理解。

内存分配

首先既然我们需要对内存进行管理,就需要知道内存是怎么分配的,是分配在哪里的?

在iOS中数据是存在在堆和栈中的,然而我们的内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。

  • 非OC对象(基础数据类型)存储在栈上
  • OC对象存储在堆上

如下面一段代码:

int a = 10; //栈
    
int b = 20; //栈
    
Car *c = [[Car alloc] init];

在内存中的表现形式如下:

image

引用计数

引用计数解释

引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。

在遥远的以前,iOS开发的内存管理是手动处理引用计数,在合适的地方使引用计数-1,直到减为0,内存释放。现在的iOS开发内存管理使用的是ARC,自动管理引用计数,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码以及在 Runtime 做一些优化。

文艺的解释

记得在《寻梦环游记》里对于一个人的死亡是这样定义的:当这个这个世界上最后一个人都忘记你时,就迎来了终极死亡。类比于引用计数,就是每有一个人记得你时你的引用计数加1,每有一个人忘记你时,你的引用计数减1,当所有人都忘记你时,你就消失了,也就是从内存中释放了。

如果再深一层,包含我们后面要介绍的ARC中的强引用和弱引用的话,那这个记住的含义就不一样了。强引用就是你挚爱的亲人,朋友等对你比较重要的人记得你,你的引用计数才加1。

而弱引用就是那种路人,一面之缘的人,他们只是对你有一个印象,他们记得你是没有用的,你的引用计数不会加1。当你挚爱的人都忘记你时,你的引用计数归零,你就从这个世界上消失了,而这些路人只是感觉到自己记忆中忽然少了些什么而已。

代码测试

我们创建一个工程,在Build Phases里设置AppDelegate的Compiler Flags-fno-objc-arc来开启手动管理引用计数的模式。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    NSObject *object = [[NSObject alloc] init];
    NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x", (unsigned long)[object retainCount], object, &object);
    self.property = object;
    NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x \n property指针内存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
    [object release];
    NSLog(@"\n 引用计数 = %lu \n 对象内存 = %p \n object指针内存地址 = %x \n property指针内存地址 = %x", (unsigned long)[object retainCount], object, &object, &_property);
    return YES;
}

输出:

2018-08-25 21:01:01.323677+0800 test[26304:9610044] 
 引用计数 = 1 
 对象内存 = 0x60000000e290 
 object指针内存地址 = ee0fee28
2018-08-25 21:01:01.323880+0800 test[26304:9610044] 
 引用计数 = 2 
 对象内存 = 0x60000000e290 
 object指针内存地址 = ee0fee28 
 property指针内存地址 = 301b8
2018-08-25 21:01:01.324088+0800 test[26304:9610044] 
 引用计数 = 1 
 对象内存 = 0x60000000e290 
 object指针内存地址 = ee0fee28 
 property指针内存地址 = 301b8

我们看到object持有对象引用计数+1为1,然后self.property又持有了对象,引用计数再+1为2,然后我们主动释放object,引用计数-1变为1。我们能看到[object release]释放后指向对象的指针仍就被保留在object这个变量中,只是对象的引用计数-1了而已。

对应的内存上的分配如下图所示:

2018-08-25 _9_08_52.png

MRC手动管理引用计数

在MRC中增加的引用计数都是需要自己手动释放的,所以我们需要知道哪些方式会引起引用计数+1;

对象操作 OC中对应的方法 引用计数的变化
生成并持有对象 alloc/new/copy/mutableCopy等 +1
持有对象 retain +1
释放对象 release -1
废弃对象 dealloc -

四个法则

  • 自己生成的对象,自己持有。
  • 非自己生成的对象,自己也能持有。
  • 不在需要自己持有对象的时候,释放。
  • 非自己持有的对象无需释放。
/*
 * 自己生成并持有该对象
 */
 id obj0 = [[NSObeject alloc] init];
 id obj1 = [NSObeject new];
/*
 * 持有非自己生成的对象
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有

[obj retain]; // 自己持有对象
/*
 * 不在需要自己持有的对象的时候,释放
 */
id obj = [[NSObeject alloc] init]; // 此时持有对象

[obj release]; // 释放对象
/*
 * 指向对象的指针仍就被保留在obj这个变量中
 * 但对象已经释放,不可访问
 */
/*
 * 非自己持有的对象无法释放
 */
id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有

[obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为

非自己生成的对象,且该对象存在,但自己不持有

其中关于非自己生成的对象,且该对象存在,但自己不持有是如何实现的呢?这个特性是使用autorelease来实现的,示例代码如下:

- (id) getAObjNotRetain {
    id obj = [[NSObject alloc] init]; // 自己持有对象
    
    [obj autorelease]; // 取得的对象存在,但自己不持有该对象
    
    return obj;
}

使用autorelease方法可以使取得的对象存在,但自己不持有对象。autorelease 使得对象在超出生命周期后能正确的被释放(通过调用release方法)。在调用 release 后,对象会被立即释放,而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放。

[NSMutableArray array] [NSArray array]都可以取得谁都不持有的对象,这些方法都是通过autorelease实现的。

ARC自动管理引用计数

ARC介绍

ARC其实也是基于引用计数,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。

现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮你做了。为什么说是大部分呢,因为底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

还有就算循环引起情况就算由于互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。

所有权修饰符

Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。

其中有以下4种所有权修饰符:

  • __strong
  • __weak
  • __unsafe_unretaied
  • __autoreleasing

所有权修饰符和属性的修饰符对应关系如下所示:

  • assign 对应的所有权类型是 __unsafe_unretained
  • copy 对应的所有权类型是 __strong
  • retain 对应的所有权类型是 __strong
  • strong 对应的所有权类型是 __strong
  • unsafe_unretained对应的所有权类型是__unsafe_unretained
  • weak 对应的所有权类型是 __weak

__strong

__strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

原理:

{
    id __strong obj = [[NSObject alloc] init];
}
//编译器的模拟代码
id obj = objc_msgSend(NSObject,@selector(alloc));
objc_msgSend(obj,@selector(init));

// 出作用域的时候调用
objc_release(obj);

虽然ARC有效时不能使用release方法,但由此可知编译器自动插入了release。

对象是通过除alloc、new、copy、multyCopy外方法产生的情况

{
    id __strong obj = [NSMutableArray array];
}

结果与之前稍有不同:

//编译器的模拟代码
id obj = objc_msgSend(NSMutableArray,@selector(array));
objc_retainAutoreleasedReturnValue(obj);
objc_release(obj);

objc_retainAutoreleasedReturnValue函数主要用于优化程序的运行。它是用于持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasePool中对象的方法,或是函数的返回值。像该源码这样,在调用array类方法之后,由编译器插入该函数。

而这种objc_retainAutoreleasedReturnValue函数是成对存在的,与之对应的函数是objc_autoreleaseReturnValue。它用于array类方法返回对象的实现上。下面看看NSMutableArray类的array方法通过编译器进行了怎样的转换:

+ (id)array
{
    return [[NSMutableArray alloc] init];
}
//编译器模拟代码
+ (id)array
{
    id obj = objc_msgSend(NSMutableArray,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    
    // 代替我们调用了autorelease方法
    return objc_autoreleaseReturnValue(obj);
}

我们可以看见调用了objc_autoreleaseReturnValue函数且这个函数会返回注册到自动释放池的对象,但是,这个函数有个特点,它会查看调用方的命令执行列表,如果发现接下来会调用objc_retainAutoreleasedReturnValue则不会将返回的对象注册到autoreleasePool中而仅仅返回一个对象。达到了一种最优效果。如下图:

__weak

__weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。使用__weak修饰的变量,即是使用注册到autoreleasePool中的对象。__weak 最常见的一个作用就是用来避免循环循环。需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

__weak 的几个使用场景:

  • 在 Delegate 关系中防止循环引用。
  • 在 Block 中防止循环引用。
  • 用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;。

原理:

    {
        id __weak obj = [[NSObject alloc] init];
    }

编译器转换后的代码如下:

    id obj;
    id tmp = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(tmp,@selector(init));
    objc_initweak(&obj,tmp);
    objc_release(tmp);
    objc_destroyWeak(&object);

对于__weak内存管理也借助了类似于引用计数表的散列表,它通过对象的内存地址做为key,而对应的__weak修饰符变量的地址作为value注册到weak表中,在上述代码中objc_initweak就是完成这部分操作,而objc_destroyWeak
则是销毁该对象对应的value。当指向的对象被销毁时,会通过其内存地址,去weak表中查找对应的__weak修饰符变量,将其从weak表中删除。所以,weak在修饰只是让weak表增加了记录没有引起引用计数表的变化。

对象通过objc_release释放对象内存的动作如下:

  1. objc_release
  2. 因为引用计数为0所以执行dealloc
  3. _objc_rootDealloc
  4. objc_dispose
  5. objc_destructInstance
  6. objc_clear_deallocating

而在对象被废弃时最后调用了objc_clear_deallocating,该函数的动作如下:

  1. 从weak表中获取已废弃对象内存地址对应的所有记录
  2. 将已废弃对象内存地址对应的记录中所有以weak修饰的变量都置为nil
  3. 从weak表删除已废弃对象内存地址对应的记录
  4. 根据已废弃对象内存地址从引用计数表中找到对应记录删除
  5. 据此可以解释为什么对象被销毁时对应的weak指针变量全部都置为nil,同时,也看出来销毁weak步骤较多,如果大量使用weak的话会增加CPU的负荷。

还需要确认一点是:使用__weak修饰符的变量,即是使用注册到autoreleasePool中的对象。

    {
        id __weak obj1 = obj; 
        NSLog(@"obj2-%@",obj1);
    }

编译器转换上述代码如下:

    id obj1; 
    objc_initweak(&obj1,obj);
    id tmp = objc_loadWeakRetained(&obj1);
    objc_autorelease(tmp);
    NSLog(@"%@",tmp);
    objc_destroyWeak(&obj1);

objc_loadWeakRetained函数获取附有__weak修饰符变量所引用的对象并retain, objc_autorelease函数将对象放入autoreleasePool中,据此当我们访问weak修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用weak的话,在我们去访问weak修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能。

解决方案:要访问weak修饰的变量时,先将其赋给一个strong变量,然后进行访问

为什么访问weak修饰的对象就会访问注册到自动释放池的对象呢?

因为weak不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在autoreleasePool销毁时释放对象占用的内存。

__unsafe_unretained

ARC 是在 iOS5 引入的,而 __unsafe_unretained 这个修饰符主要是为了在ARC刚发布时兼容iOS4以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义property时对应的是unsafe_unretained__unsafe_unretained 修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。

__unsafe_unretained的应用场景:

在 ARC 环境下但是要兼容 iOS4.x 的版本,用__unsafe_unretained 替代 __weak 解决强循环循环的问题。

__autoreleasing

将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。

    @autoeleasepool {
        // 如果看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了 
        id __autoreleasing obj = [[NSObject alloc] init];
    }

编译器转换上述代码如下:

    id pool = objc_autoreleasePoolPush(); 
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);
@autoreleasepool {
        id __autoreleasing obj = [NSMutableArray array];
    }

编译器转换上述代码如下:

    id pool = objc_autoreleasePoolPush();
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);

上面两种方式,虽然第二种持有对象的方法从alloc方法变为了objc_retainAutoreleasedReturnValue函数,都是通过objc_autorelease,注册到autoreleasePool中。

循环引用

什么是循环引用?循环引用就是在两个对象互相之间强引用了,引用计数都加1了,我们前面说过,只有当引用计数减为0时对象才释放。但是这两个的引用计数都依赖于对方,所以也就导致了永远无法释放。

最容易产生循环引用的两种情况就是DelegateBlock。所以我们就引入了弱引用这种概念,即弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。也就是我们上面所说的所有权修饰符__weak的作用。关于原理在__weak部分也有描述,简单的描述就是每一个拥有弱引用的对象都有一张表来保存弱引用的指针地址,但是这个弱引用并不会使对象引用计数加1,所以当这个对象的引用计数变为0时,系统就通过这张表,找到所有的弱引用指针把它们都置成nil。

所以在ARC中做内存管理主要就是发现这些内存泄漏,关于内存泄漏Instrument为我们提供了 Allocations/Leaks 这样的工具用来检测。但是个人觉得还是很麻烦的,大部分时候内存泄漏并不会引起应用的崩溃或者报错之类的,所以我们也不会每次主动的去查看当前代码有没有内存泄漏之类的。

这里有一个微信读书团队开源的工具MLeaksFinder,它可以在你程序运行期间,如果有内存泄漏就会弹出提示告诉你泄漏的地方。

具体原理如下

我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}
- (void)assertNotDealloc {
     NSAssert(NO, @“”);
}

这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会中断言。

Core Foundation 对象的内存管理

底层的 Core Foundation 对象,在创建时大多以 XxxCreateWithXxx 这样的方式创建,例如:

// 创建一个 CFStringRef 对象
CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);

// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

对于这些对象的引用计数的修改,要相应的使用 CFRetain 和 CFRelease 方法。如下所示:

// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

// 引用计数加 1
CFRetain(fontRef);

// 引用计数减 1
CFRelease(fontRef);

对于 CFRetainCFRelease 两个方法,读者可以直观地认为,这与 Objective-C 对象的 retainrelease 方法等价。

所以对于底层 Core Foundation 对象,我们只需要延续以前手工管理引用计数的办法即可。

除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:

  • __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。

  • __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。

  • __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

参考

iOS中堆和栈的区别

Objective-C 中的内存分配

iOS内存管理

理解 iOS 的内存管理

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

推荐阅读更多精彩内容