iOS开发基础26-内存管理进阶:autoreleasepool、weak 底层、Tagged Pointer、Block 内存与循环引用检测

iOS 内存管理进阶:autoreleasepool、weak 底层、Tagged Pointer、Block 内存与循环引用检测


一、autoreleasepool 底层原理

1. @autoreleasepool 的编译后实现

@autoreleasepool {} 在编译后会被替换为 Runtime 函数调用:

// 源码
@autoreleasepool {
    NSString *str = [NSString stringWithFormat:@"%d", 1];
}

// 编译后(等价实现)
void *poolToken = objc_autoreleasePoolPush();
NSString *str = [NSString stringWithFormat:@"%d", 1]; // 内部调用 autorelease
objc_autoreleasePoolPop(poolToken); // pop 时释放池中所有 autoreleased 对象

2. AutoreleasePoolPage 结构

自动释放池不是简单的数组,而是一个以 AutoreleasePoolPage 为节点的双向链表(栈结构):

AutoreleasePoolPage (双向链表)
├── parent: 指向前一个 page
├── child: 指向后一个 page
├── next:  指向下一个可存储对象的位置
└── 存储区: 存放 autoreleased 对象指针(每页约 4096 字节)
  • 每个 page 固定大小为一页内存(4096 字节),前 56 字节存储元数据,剩余空间存储对象指针。
  • 一个 page 存满后自动创建下一个 page,形成链表。
  • push 操作在当前 page 插入一个哨兵对象(POOL_BOUNDARY),返回哨兵位置作为 token。
  • pop 操作从 next 位置往回逐个调用 release,直到遇到哨兵对象。

3. 线程与 autoreleasepool

  • 每个线程有独立的 autoreleasepool 栈,线程间互不影响。
  • 主线程:RunLoop 在每次循环开始时自动 push 一个 pool,循环结束时 pop,释放本次循环产生的所有 autoreleased 对象。
  • 子线程:默认没有 RunLoop,也不会自动创建 pool。如果子线程中产生 autoreleased 对象,必须手动包裹 @autoreleasepool,否则对象会泄漏(没有 pool 来释放它)。
// 子线程中必须手动创建 autoreleasepool
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @autoreleasepool {
        for (NSInteger i = 0; i < 10000; i++) {
            NSString *str = [NSString stringWithFormat:@"%ld", i];
            // str 是 autoreleased,由 pool 管理
        }
    } // pool 销毁,所有 str 被 release
});

4. 手动创建 autoreleasepool 的场景

场景 原因
循环内产生大量临时对象 降低内存峰值,避免循环结束前对象堆积
子线程中产生 autoreleased 对象 子线程无自动 pool,不创建会泄漏
非主线程 RunLoop 中 需手动管理 pool 生命周期
// 循环内降低内存峰值
for (NSInteger i = 0; i < 100000; i++) {
    @autoreleasepool {
        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"img_%ld", i]];
        // 处理 image...
    } // 每次循环结束释放本次的 autoreleased 对象
}

没有 @autoreleasepool 时,这 10 万个 autoreleased 对象会堆积到当前 RunLoop 循环结束才释放,内存峰值极高。


二、weak 引用底层实现

1. weak 表结构

weak 引用不存储在对象内部,而是存储在全局的 SideTable 中。每个 SideTable 包含:

SideTable
├── spinlock_t slock:        自旋锁,保护 weak 表和引用计数表
├── RefcountMap refcnts:     引用计数表(对象地址 → 引用计数)
└── weak_table_t weak_table: weak 引用表
    ├── weak_entry_t *weak_entries: 哈希桶数组
    ├── num_entries:                 条目数量
    └── mask:                        哈希掩码
        └── 每个 weak_entry_t 存储一个对象的所有 weak 指针地址

2. weak 引用的注册与读取

// 源码
__weak Person *weakPerson = person;

// 编译后调用 Runtime 函数
Person *weakPerson = nil;
objc_initWeak(&weakPerson, person);
// 内部调用 objc_storeWeak,在 weak 表中注册 weakPerson 的地址与 person 的映射

读取 weak 引用时:

// 源码
Person *p = weakPerson;

// 编译后
Person *p = objc_loadWeakRetained(&weakPerson);
// 内部:从 weak 表查找对象,执行 retain + autorelease,返回对象
// 如果对象已销毁,weakPerson 已被置 nil,返回 nil

weak 引用的读取涉及锁操作和引用计数操作,性能比 strong 引用慢。高频访问的 weak 引用建议先赋值给 strong 局部变量(weak-strong dance)。

3. 对象销毁时 weak 置 nil 流程

对象销毁时,Runtime 遍历 weak 表,将所有指向该对象的 weak 指针置 nil:

对象销毁流程中 weak 清理部分:
  objc_destructInstance
    → weak_clear_no_lock(weak_table, obj)
      1. 根据对象地址在 weak 表中哈希查找 weak_entry
      2. 遍历该 entry 中所有 weak 指针地址
      3. 将每个 weak 指针的值置为 nil
      4. 从 weak 表中移除该 entry

这就是 weak 引用能自动置 nil 的底层原因。unsafe_unretained 不经过 weak 表,对象销毁后指针仍指向已释放内存,成为野指针。

4. weak 引用的注意事项

特性 说明
线程安全 weak 表操作有自旋锁保护,多线程安全
性能开销 注册、读取、销毁都有锁和哈希查找开销,比 strong 慢
只能修饰对象 weak 只能修饰 OC 对象类型,不能修饰基本数据类型
不能修饰非 OC 对象 CoreFoundation 对象不支持 weak(用 __unsafe_unretained)

三、Tagged Pointer 小对象优化

1. 什么是 Tagged Pointer

Tagged Pointer 是一种小对象优化技术:对于某些小型值对象(如 NSNumber、短 NSString、NSDate),不分配堆内存,而是将数据直接编码在指针本身中。指针的特殊位标记它是 Tagged Pointer 而非真实对象指针。

普通对象指针(arm64):
  0x0000000100123456 → 指向堆内存中的对象

Tagged Pointer(arm64):
  最低位 = 1(标记为 Tagged Pointer)
  第 1-3 位 = 类型标记(NSNumber / NSString / NSDate / UIColor 等)
  剩余位 = 实际数据(如数值、字符串内容)

2. 支持 Tagged Pointer 的类

类 说明
NSNumber 小整数(如 @1、@YES)直接存在指针中
NSString 短字符串(NSTaggedPointerString,通常 ≤ 10 字符)
NSDate 某些时间值
UIColor 标准颜色(如 redColor、blueColor)
NSIndexPath 小索引路径

3. Tagged Pointer 的特性

NSNumber *n1 = @1;
NSNumber *n2 = @1;
NSLog(@"%p %p", n1, n2); // 地址相同,且最低位为 1(Tagged Pointer 标记)
NSLog(@"%@", @([n1 retainCount])); // 引用计数无意义,输出很大的值或 0
特性 说明
不分配堆内存 数据存在指针中,无需 malloc/free
引用计数无意义 objc_retain/objc_release 对 Tagged Pointer 直接返回,不操作引用计数
访问极快 无需解引用指针,直接从指针位域解码数据
内存泄漏检测失效 Leaks 工具无法检测 Tagged Pointer(因为不分配堆内存)
无 weak 引用 Tagged Pointer 不经过 weak 表,__weak 指向它时直接保留指针值

4. 对内存管理的影响

Tagged Pointer 对象的存在意味着:

  • 引用计数为 0 不代表所有对象都会调用 dealloc(Tagged Pointer 不经过 dealloc 流程)。
  • retainCount 的值对 Tagged Pointer 无意义,不要依赖 retainCount 调试内存。
  • 短字符串和小数字的创建/销毁几乎零成本,无需担心频繁创建的性能问题。

四、Block 的内存管理

1. Block 的三种类型

类型 存储位置 触发条件 copy 行为
NSGlobalBlock 全局区(类似单例) 不捕获外部变量 copy 后仍是自身,无变化
NSStackBlock 栈上 捕获了外部变量 copy 后变为 NSMallocBlock(迁移到堆上)
NSMallocBlock 堆上 对 StackBlock 执行 copy 后 copy 后引用计数 +1
// NSGlobalBlock:不捕获外部变量
void (^globalBlock)(void) = ^{
    NSLog(@"global");
};
NSLog(@"%@", NSStringFromClass([globalBlock class])); // __NSGlobalBlock__

// NSStackBlock:捕获外部变量(MRC 下明显,ARC 下可能自动 copy)
int value = 10;
void (^stackBlock)(void) = ^{
    NSLog(@"%d", value); // 捕获了 value
};
// MRC 下是 __NSStackBlock__,ARC 下作为强引用时自动 copy 为 __NSMallocBlock__

ARC 下的自动 copy:ARC 环境中,Block 被强引用(赋值给 strong 属性、作为参数传递、被 copy 修饰的属性持有)时,编译器会自动插入 copy,将 StackBlock 迁移到堆上。因此 ARC 下通常看到的是 NSMallocBlock 或 NSGlobalBlock,很少看到 NSStackBlock。

2. Block 捕获变量的内存管理

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

// Block 捕获 OC 对象
Person *person = [[Person alloc] init];
person.name = @"Tom";

void (^block)(void) = ^{
    NSLog(@"%@", person.name); // Block 强引用 person(引用计数 +1)
};
// person 引用计数:2(外部变量 + Block 内部)
block();
// block 销毁时,内部强引用释放,person 引用计数 -1
捕获类型 Block 对其引用 说明
基本数据类型(int/BOOL) 值拷贝 Block 内部保存值的副本,外部修改不影响内部
OC 对象(__strong) 强引用 Block 强引用对象,引用计数 +1
OC 对象(__weak) 弱引用 Block 弱引用对象,不增加引用计数
__block 修饰的变量 强引用(包装为结构体) 可在 Block 内外读写同一变量

3. __block 底层实现

__block 修饰的变量可以在 Block 内部修改,其底层是将变量包装为一个 __Block_byref 结构体:

// 源码
__block int count = 0;
void (^block)(void) = ^{
    count = 10; // 修改 __block 变量
};
block();
NSLog(@"%d", count); // 10

// 编译后等价结构
struct __Block_byref_count_0 {
    void *__isa;
    __Block_byref_count_0 *__forwarding; // 指向自身(堆上拷贝后指向堆上的副本)
    int __flags;
    int __size;
    int count; // 实际变量值
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0 *Desc;
    __Block_byref_count_0 *count; // Block 持有 __block 结构体指针
};
  • __block 变量被包装为 __Block_byref 结构体,存储在堆上。
  • Block 和外部作用域都持有这个结构体的指针,实现读写共享。
  • __forwarding 指针确保栈上和堆上的副本访问的是同一个值(Block copy 到堆上时,__forwarding 指向堆上的副本)。
  • __block 修饰的 OC 对象默认是强引用(ARC 下),需注意循环引用。

4. Block 循环引用

// 循环引用:self 强引用 block,block 强引用 self
@interface ViewController ()
@property (nonatomic, copy) void (^completionBlock)(void);
@end

@implementation ViewController
- (void)setup {
    self.completionBlock = ^{
        [self doSomething]; // Block 强引用 self → 循环引用!
    };
}
- (void)doSomething {}
@end

解决方案:Weak-Strong Dance

__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return; // self 已释放,直接返回
    [strongSelf doSomething];
};

如果 Block 内需要多次访问 self 或有异步嵌套,使用 __strong 持有 self 防止执行过程中 self 被释放;如果只需一次访问,直接用 weakSelf 即可。


1. 问题根源

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // timer 强引用 target=self,RunLoop 强引用 timer
    // self 强引用 timer → 循环引用!self 永远无法释放
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
}
- (void)tick {}
- (void)dealloc {
    [self.timer invalidate]; // 永远不会执行,因为 self 无法释放
}
@end

引用链:RunLoop → NSTimer → target(self) → NSTimer,形成闭环。

CADisplayLink 有完全相同的问题(RunLoop → CADisplayLink → target(self) → CADisplayLink)。

2. 方案一:NSProxy 弱引用代理

NSProxy 是一个抽象基类,专门用于消息转发。创建一个弱引用 target 的 Proxy,timer 强引用 Proxy,Proxy 弱引用 target,打破循环:

// WeakProxy.h
@interface WeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

// WeakProxy.m
@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target {
    WeakProxy *proxy = [WeakProxy alloc];
    proxy->_target = target;
    return proxy;
}

// 消息转发:将消息转发给 target
- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [_target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:_target];
}
@end

使用:

// timer 强引用 proxy,proxy 弱引用 self,无循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                target:[WeakProxy proxyWithTarget:self]
                                              selector:@selector(tick)
                                              userInfo:nil
                                               repeats:YES];

业界优秀实现:YYWeakProxy(ibireme 开源),比上述基础实现更完善,处理了 respondsToSelector:、协议等边界情况。

3. 方案二:iOS 10+ Block-based Timer

iOS 10 引入了 Block 形式的 timer,target 不再是 self,而是 Block 内部弱引用 self:

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                              repeats:YES
                                                block:^(NSTimer * _Nonnull timer) {
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf tick];
}];

Block-based timer 的 target 是 timer 内部的一个 block 拷贝对象,不再直接强引用 self。Block 内用 __weak 引用 self,打破循环引用。但仍需在适当时候调用 invalidate 停止 timer。

4. 方案三:手动 invalidate(适用于可控生命周期)

在视图消失时主动 invalidate,打破 RunLoop 对 timer 的强引用:

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.timer invalidate];
    self.timer = nil;
}

此方案适用于视图消失时确实需要停止 timer 的场景。如果视图消失后仍需 timer 运行(如后台播放),则不适用。

5. invalidate 的注意事项

注意点 说明
必须在注册的 RunLoop 线程调用 invalidate 必须在 timer 注册的那个 RunLoop 所在线程调用,否则可能不生效
调用后 timer 立即失效 invalidate 后 timer 从 RunLoop 移除,强引用解除
重复调用安全 对已 invalidate 的 timer 再次调用无副作用
必须赋值为 nil invalidate 后将属性置 nil,解除 self 对 timer 的强引用

六、dealloc 底层销毁流程

对象引用计数归 0 时,Runtime 执行完整的销毁流程:

引用计数归 0
    ↓
objc_release → _objc_rootRelease → 引用计数 -1,归 0 时调用 dealloc
    ↓
[obj dealloc](开发者可重写,ARC 下自动调用 [super dealloc])
    ↓
_objc_rootDealloc(obj)
    ↓
object_dispose(obj)
    ↓
objc_destructInstance(obj)
    ├── 1. cxx_destruct
    │       ├── 调用 C++ 成员变量的析构函数
    │       └── 释放 __block 变量(__Block_byref 结构体)
    ├── 2. objc_clear_deallocating
    │       └── 设置对象标记为"正在销毁",阻止新的 weak 引用注册和关联对象添加
    ├── 3. weak_clear_no_lock
    │       └── 遍历 weak 表,将所有指向该对象的 weak 指针置 nil
    └── 4. association_remove_all
            └── 移除该对象的所有关联对象(objc_setAssociatedObject 设置的)
    ↓
free(obj) → 释放堆内存

各阶段说明

阶段 说明
cxx_destruct 由编译器自动生成,调用所有 C++ 成员变量的析构函数,并释放 __block 变量。ARC 下成员变量的 release 也在此阶段完成
objc_clear_deallocating 标记对象正在销毁,此时如果有新的 weak 引用指向该对象会直接置 nil,添加关联对象会被忽略
weak_clear_no_lock 核心步骤:遍历 weak 表中该对象的所有 weak 指针,逐个置 nil。这是 weak 自动置 nil 的底层实现
association_remove_all 移除所有通过 objc_setAssociatedObject 设置的关联对象,并执行对应的 release/copy 清理
free 最终释放对象占用的堆内存

dealloc 的执行顺序:子类的 dealloc 先执行(ARC 下自动调用 [super dealloc]),然后父类的 dealloc 依次执行,最后到 NSObject 的 dealloc 触发 _objc_rootDealloc 进入底层销毁流程。


七、循环引用检测与内存分析工具

1. Instruments Leaks

Xcode 自带的内存泄漏检测工具:

  • 打开方式:Xcode → Product → Profile → 选择 Leaks → 启动录制
  • 原理:定时扫描堆内存,查找没有被任何根对象引用的对象(即泄漏对象)
  • 使用技巧:
    • 进入可疑页面后操作,再退出,观察是否有对象未释放
    • Leaks 检测到泄漏后,点击可查看引用历史(Retain/Release 调用栈)
    • 配合 Allocations 工具查看对象的创建和销毁情况

2. FBRetainCycleDetector

Facebook 开源的循环引用检测库,运行时自动检测循环引用:

// 仅在 Debug 模式下启用
#ifdef DEBUG
#import <FBRetainCycleDetector/FBRetainCycleDetector.h>

FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self];
NSSet *cycles = [detector findRetainCycles];
NSLog(@"检测到循环引用: %@", cycles);
#endif
  • 原理:从候选对象出发,深度遍历强引用图,查找回到自身的路径(即循环)
  • 优点:运行时检测,能定位到具体的循环引用链
  • 缺点:有性能开销,仅建议 Debug 模式使用

3. MLeaksFinder

腾讯开源的内存泄漏检测库,无需手动操作,自动检测 UIViewController 和 UIView 的泄漏:

  • 集成后,UIViewController pop/dismiss 后会自动检测该控制器及其子视图是否在 2 秒后释放
  • 检测到泄漏时弹出 Alert 提示,并打印泄漏对象的信息
  • 可配合 FBRetainCycleDetector 进一步定位循环引用链
  • 零侵入,Debug 模式自动生效,Release 模式自动禁用

4. Allocations

Instruments 中的内存分配分析工具:

  • 跟踪所有对象的创建、存活、销毁
  • 可按类名筛选,查看特定类的实例数量和内存占用
  • Mark Generation:标记时间点,对比两个时间点之间的内存增长,定位泄漏
  • 配合 #import <Foundation/Foundation.h> 的 NSStringFromClass 调试

5. 调试辅助代码

// 在 dealloc 中打印,确认对象是否释放
- (void)dealloc {
    NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}

// 检测指定类的实例数量(Debug 辅助)
#ifdef DEBUG
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSel = @selector(init);
        // 通过 Method Swizzling 统计 init 和 dealloc 次数
    });
}
#endif

八、Swift 内存管理补充

1. 值类型与引用类型

类型 存储位置 内存管理 示例
值类型(struct/enum) 栈上 无需引用计数,作用域结束自动销毁 Int、String、Array、Dictionary、自定义 struct
引用类型(class) 堆上 ARC 引用计数管理 UIViewController、自定义 class
// 值类型:拷贝,互不影响
struct Point { var x: Int; var y: Int }
var p1 = Point(x: 1, y: 2)
var p2 = p1 // 值拷贝
p2.x = 10
print(p1.x) // 1,p1 不受影响

// 引用类型:共享,互相影响
class Person { var name: String; init(name: String) { self.name = name } }
let person1 = Person(name: "Tom")
let person2 = person1 // 引用拷贝,指向同一对象
person2.name = "Jerry"
print(person1.name) // Jerry,person1 被影响

2. Copy-on-Write(写时复制)

Swift 的标准库值类型(Array、String、Dictionary、Set)实现了写时复制优化:

var array1 = [1, 2, 3]
var array2 = array1 // 此时不复制,共享底层存储
array2.append(4)    // 修改时才真正复制底层存储(copy-on-write)
// array1 = [1, 2, 3],array2 = [1, 2, 3, 4]
  • 赋值时不立即复制,共享底层存储(引用计数管理)
  • 修改时检查引用计数,如果被多个变量共享则复制一份再修改
  • 大幅优化值类型的性能和内存占用

3. @escaping 与非逃逸闭包

类型 说明 内存管理
非逃逸闭包(默认) 闭包在函数返回前执行完毕,不会被存储 无需循环引用考虑,闭包执行完即释放
逃逸闭包(@escaping) 闭包会被存储到属性或变量中,函数返回后才执行 需注意循环引用,使用 [weak self]
// 非逃逸闭包:函数返回前执行,无需 weak
func syncTask(completion: () -> Void) {
    completion() // 同步执行
}

// 逃逸闭包:被存储,函数返回后执行,需要 weak
var storedCompletion: (() -> Void)?
func asyncTask(completion: @escaping () -> Void) {
    storedCompletion = completion // 闭包被存储,逃逸了
}

// 使用逃逸闭包时注意循环引用
class ViewController {
    func loadData() {
        asyncTask { [weak self] in // [weak self] 避免循环引用
            self?.updateUI()
        }
    }
    func updateUI() {}
}

4. async/await 与 Task 的内存管理

Swift 并发中的 Task 闭包默认强引用捕获的对象:

class ViewController {
    func fetchData() {
        // Task 闭包强引用 self,Task 被系统持有直到完成
        // 如果 Task 永不完成(如无限等待),self 会泄漏
        Task {
            let data = await API.fetch()
            self.updateUI(data) // 强引用 self
        }
    }
    
    func fetchDataSafe() {
        Task { [weak self] in // 显式 weak 引用
            let data = await API.fetch()
            self?.updateUI(data)
        }
    }
    
    func updateUI(_ data: Data) {}
}

Task 的闭包是逃逸闭包,被系统的并发调度器持有直到任务完成。对于可能长时间运行或取消的 Task,建议使用 [weak self]。


九、总结

  • autoreleasepool:以 AutoreleasePoolPage 双向链表实现,每个线程独立,主线程由 RunLoop 自动管理,子线程需手动创建;循环内大量临时对象时手动 @autoreleasepool 降低内存峰值。
  • weak 底层:weak 引用存储在全局 SideTable 的 weak_table_t 中,对象销毁时 weak_clear_no_lock 遍历将所有 weak 指针置 nil;读写有锁开销,高频访问建议 weak-strong dance。
  • Tagged Pointer:小对象(NSNumber、短 NSString、UIColor 等)不分配堆内存,数据编码在指针中,引用计数无意义,访问极快。
  • Block 内存:三种类型(Global/Stack/Malloc),ARC 下强引用时自动 copy 到堆;__block 变量包装为 __Block_byref 结构体实现读写共享;Block 循环引用用 weak-strong dance 解决。
  • NSTimer 泄漏:根源是 RunLoop→timer→target(self)→timer 循环引用;解决方案包括 NSProxy 弱引用代理(YYWeakProxy)、iOS 10+ Block-based timer、viewWillDisappear 中 invalidate。
  • dealloc 流程:dealloc → _objc_rootDealloc → object_dispose → objc_destructInstance(cxx_destruct → clear_deallocating → weak_clear_no_lock → association_remove_all)→ free。
  • 检测工具:Instruments Leaks/Allocations、FBRetainCycleDetector、MLeaksFinder,配合 dealloc 打印调试。
  • Swift 补充:值类型在栈上无需引用计数,引用类型在堆上用 ARC;写时复制优化标准库值类型;@escaping 闭包和 Task 需注意循环引用,用 [weak self]。

posted @ 2015-07-28 23:01  Mr.陳  阅读(331)  评论(0)    收藏  举报