iOS开发之缓存框架、内存缓存、磁盘缓存、NSCache、TMMemoryCache、PINMemoryCache、YYMemoryCache、TMDiskCache、PINDiskCache

1.在项目中我们难免会用到一些缓存方式来保存服务器传过来的数据,以减少服务器的压力。 缓存的方式分为两种分别为内存缓存和磁盘缓存,内存缓存速度快容量小,磁盘缓存容量大速度慢可持久化。常见的内存缓存有NSCache、TMMemoryCachePINMemoryCacheYYMemoryCache。常见的磁盘缓存有TMDiskCache、PINDiskCache、YYCache.

1.本文章着重讲下YYCache。 这是为什么呢,因为他比其他的缓存框架更加高效,使用方便。

YYCache: 去掉了异步访问的接口,尽量优化了同步访问的性能,用 OSSpinLock 来保证性能安全。另外,缓存内部用双向链表和 NSDictionary
实现了 LRU 淘汰算法.体现高效的部分,也是采用的 SQLite 配合文件的存储方式。在存取小数据 (NSNumber) 时,YYDiskCache 的性能远远高出基于文件存储的库;而较大数据的存取性能则比较接近了。但得益于 SQLite 存储的元数据,YYDiskCache 实现了 LRU 淘汰算法、更快的数据统计,更多的容量控制选项。

YYCache的使用:

上图是YYCache的相关类文件。

重点介绍:

1. 内存缓存(YYMemoryCache)

存储的单元是_YYLinkedMapNode,除了key和value外,还存储了它的前后Node的地址_prev,_next.

整个实现基于_YYLinkedMap,它是一个双向链表,除了存储了字典_dic外,还存储了头结点和尾节点.它实现的功能很简单,就是:有新数据了插入链表头部,访问过的数据结点移到头部,内存紧张时把尾部的结点移除.就这样实现了淘汰算法.

因为内存访问速度很快,锁占用的时间少,所以用的速度最快的OSSpinLockLock

部分源码解析:

- (instancetype)init {
    self = super.init;
    pthread_mutex_init(&_lock, NULL);  //初始化互斥锁的方法  ,在dealloc方法中进行回收
    _lru = [_YYLinkedMap new];
    _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);

    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _autoTrimInterval = 5.0;
    _shouldRemoveAllObjectsOnMemoryWarning = YES;
    _shouldRemoveAllObjectsWhenEnteringBackground = YES;

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];   //注册内存警告的通知,方便回收
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];   //注册程序进入后台的通知,可以回收内存。

    [self _trimRecursively];
    return self;
}

删除某个值的操作

- (void)removeObjectForKey:(id)key {
    if (!key) return;
    pthread_mutex_lock(&_lock); //加锁,保证线程安全
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //获取对应key的node
    if (node) {
        [_lru removeNode:node];         //移除这个节点
        if (_lru->_releaseAsynchronously) {   //判断当前是异步线程还是多线程
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();  //
            dispatch_async(queue, ^{
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    pthread_mutex_unlock(&_lock);  //解锁
}

增加一对键值相关代码 //解释在注释中

<span style="font-family:Open Sans, sans-serif;color:#373737;">- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    pthread_mutex_lock(&_lock); //加锁
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //取出对应key的node
    NSTimeInterval now = CACurrentMediaTime();   //保存一个时间戳,把最新的修改时间或添加保存进入node中。
    if (node) {   //存在的操作
        _lru->_totalCost -= node->_cost;   //把原有缓存内存开销减去
        _lru->_totalCost += cost;          //加入新的内存开销
        node->_cost = cost;                //指定内存开销
        node->_time = now;                 //修改的最新时间
        node->_value = object;             // 赋值
        [_lru bringNodeToHead:node];       // 修改操作,</span><span style="font-family:Helvetica Neue, Helvetica, STheiti, 微软雅黑, 黑体, Arial, Tahoma, sans-serif, serif;color:#252525;">把修改node放到双链表的头部。</span><span style="font-family:Open Sans, sans-serif;color:#373737;">
    } else { //不存在次node的操作
        node = [_YYLinkedMapNode new];    //创建一个 </span><span style="color: rgb(55, 55, 55); font-family: 'Open Sans', sans-serif;">_YYLinkedMapNode</span><span style="font-family:Open Sans, sans-serif;color:#373737;">
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead:node];        //插入操作,把最新的node放到双链表的头部
    }
    if (_lru->_totalCost > _costLimit) { //如果内存花销超出了最大限制的内存大小
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];             //删除双链表的尾部节点
        });
    }
    if (_lru->_totalCount > _countLimit) {   //如果超出缓存数量,也是</span><span style="color: rgb(55, 55, 55); font-family: 'Open Sans', sans-serif;">删除双链表的尾节点</span><span style="font-family:Open Sans, sans-serif;color:#373737;">
        _YYLinkedMapNode *node = [_lru removeTailNode];
        if (_lru->_releaseAsynchronously) { //当前线程的判断
            dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
            dispatch_async(queue, ^{
                [node class]; //hold and release in queue
            });
        } else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [node class]; //hold and release in queue
            });
        }
    }
    pthread_mutex_unlock(&_lock);   //解锁
}
</span>

查询某个key

- (id)objectForKey:(id)key {
    if (!key) return nil;
    pthread_mutex_lock(&_lock);  //加锁
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
        node->_time = CACurrentMediaTime();  //更改当前node的时间
        [_lru bringNodeToHead:node]; //把查找到的node,移到双向链表的头部
    }
    pthread_mutex_unlock(&_lock); //解锁
    return node ? node->_value : nil;  //利用三目运算符,进行返回value or nil。
}

总结:(三种常见的操作)

1. 插入: 没有对应key,只需要把新的数据插入到双链表的头部

2. 替换: 有对应的key, 修改时间,value,并把数据移到双链表的头部,加上是否超出内存大小限制,是否超出文件数量限制,如果有,就移除双链表的尾节点。

3. 查找: 有对应的key,返回值,并修改node的time,并把数据移到双链表的头部, 无对应的key,返回nil   。

2. 硬盘缓存(YYDiskCache)

采用的是文件和数据库相互配合的方式.

有一个参数inlineThreshold,默认20KB,小于它存数据库,大于它存文件.能获得效率的提高.

key:path,value:cache存储在NSMapTable里.根据path获得cache,进行一系列的set,get,remove操作

更底层的是YYKVStorage,它能直接对sqlite和文件系统进行读写.

每次内存超过限制时,select key, filename, size from manifest order by last_access_time desc limit ?1

会根据时间排序来删除最近不常用的数据.

硬盘访问的时间比较长,如果用OSSpinLockLock锁会造成CPU消耗过大,所以用的dispatch_semaphore_wait来做.

YYDiskCache的核心部分是YYKVStorage.

YYKVStorage解析:不直接使用,通过YYDiskCache调用。

1. 新增OR替换操作

- (BOOL)saveItem:(YYKVStorageItem *)item {
    return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];
}

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {
    return [self saveItemWithKey:key value:value filename:nil extendedData:nil];
}

- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
    if (key.length == 0 || value.length == 0) return NO;
    if (_type == YYKVStorageTypeFile && filename.length == 0) {
        return NO;
    }

    if (filename.length) {   //保存文件操作
        if (![self _fileWriteWithName:filename data:value]) {         //保存到文件
            return NO;
        }
        if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) {   //保存到数据库如果失败,就删除掉这个文件名的文件
            [self _fileDeleteWithName:filename];
            return NO;
        }
        return YES;
    } else {
        if (_type != YYKVStorageTypeSQLite) {   //文件保存方式
            NSString *filename = [self _dbGetFilenameWithKey:key];  //获取文件名
            if (filename) {
                [self _fileDeleteWithName:filename];         //删除这个文件
            }
        }
        return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];  //保存到数据库
    }
}

文件保存操作

- (BOOL)_fileWriteWithName:(NSString *)filename data:(NSData *)data {
    NSString *path = [_dataPath stringByAppendingPathComponent:filename];
    return [data writeToFile:path atomically:NO];      //写入文件
}

数据库保存操作

- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
    NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];          //把sql编译成二进制 ,stmt辅助类型
    if (!stmt) return NO;

    int timestamp = (int)time(NULL);
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);    //绑定key
    sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);  //绑定 filename
    sqlite3_bind_int(stmt, 3, (int)value.length);               //绑定value
    if (fileName.length == 0) {
        sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
    } else {
        sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
    }
    sqlite3_bind_int(stmt, 5, timestamp);
    sqlite3_bind_int(stmt, 6, timestamp);
    sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
    //以上都是绑定各种参数
    int result = sqlite3_step(stmt);       //执行sql语句
    if (result != SQLITE_DONE) {
        if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}

2. 查找操作

- (YYKVStorageItem *)getItemForKey:(NSString *)key {
    if (key.length == 0) return nil;
    YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];  //从数据库返回对应的对象
    if (item) {
        [self _dbUpdateAccessTimeWithKey:key];           //更新这个key对应的对象时间
        if (item.filename) {
            item.value = [self _fileReadWithName:item.filename];       //从文件中取得对应key的value,并赋值
            if (!item.value) {<span style="white-space:pre">					</span>       //如果value不存在,就把对应的key删除掉
                [self _dbDeleteItemWithKey:key];
                item = nil;
            }
        }
    }
    return item;
}

数据库查询操作

- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
    NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";                                                //sql语句拼接,通过key去查看对应的YYKVStorageItem.
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
    if (!stmt) return nil;
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);              //绑定参数
    YYKVStorageItem *item = nil;
    int result = sqlite3_step(stmt);
    if (result == SQLITE_ROW) {                                 //查询成功的值
        item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData];       //通过这个方法把打包好的YYKVStorageItem,进行赋值返回
    } else {
        if (result != SQLITE_DONE) {
            if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        }
    }
    return item;
}

文件查询操作

- (NSData *)_fileReadWithName:(NSString *)filename {
    NSString *path = [_dataPath stringByAppendingPathComponent:filename];   //通过文件名,拼接路径
    NSData *data = [NSData dataWithContentsOfFile:path];                    //返回对应的data
    return data;
}

3. 删除操作

- (BOOL)removeItemForKey:(NSString *)key {
    if (key.length == 0) return NO;
    switch (_type) {
        case YYKVStorageTypeSQLite: {
            return [self _dbDeleteItemWithKey:key];  //数据库缓存
        } break;
        case YYKVStorageTypeFile:
        case YYKVStorageTypeMixed: {
            NSString *filename = [self _dbGetFilenameWithKey:key]; //获取文件名
            if (filename) {
                [self _fileDeleteWithName:filename];   //文件缓存
            }
            return [self _dbDeleteItemWithKey:key];    //数据库缓存
        } break;
        default: return NO;
    }
}

数据库缓存删除

- (BOOL)_dbDeleteItemWithKey:(NSString *)key {
    NSString *sql = @"delete from manifest where key = ?1;";  //删除sql语句
    sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];           //二进制数据库辅助类型stmt
    if (!stmt) return NO;
    sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);     //参数绑定

    int result = sqlite3_step(stmt);                          //执行
    if (result != SQLITE_DONE) {                              //结果 SQLITE_DONE 为成功
        if (_errorLogsEnabled) NSLog(@"%s line:%d db delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
        return NO;
    }
    return YES;
}

文件缓存

- (BOOL)_fileDeleteWithName:(NSString *)filename {
    NSString *path = [_dataPath stringByAppendingPathComponent:filename];
    return [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];   //删除操作
}

那肯定有人问,咦, 怎么YYCache类,还没有冒头呀。 哈哈,这个是我们最终使用的类哟。 也就是说,上面的什么内存缓存呀,磁盘缓存呀,我们压根就不是直接使用它们的,我们使用封装好它们的YYCache类。 下面就开始介绍YYCache类。

直接上代码:

@interface YYCache : NSObject
// 读取当前数据库名称
@property (copy, readonly) NSString *name;

@property (strong, readonly) YYMemoryCache *memoryCache; <span style="font-family: Arial, Helvetica, sans-serif;">//内存缓存</span>
@property (strong, readonly) YYDiskCache *diskCache;     //<span style="font-family: Arial, Helvetica, sans-serif;">文件缓存</span>

// 可通过下面三种方法来实例化YYCache对象
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
+ (nullable instancetype)cacheWithPath:(NSString *)path;

// 禁止通过下面两个方式实例化对象
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new __attribute__((unavailable("new方法不可用,请用initWithName:")));

// 通过key判断是否缓存了某个东西,第二个法是异步执行,异步回调
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;

// 读--通过key读取缓存,第二个法是异步执行,异步回调
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;

// 增、改--缓存对象(可缓存遵从NSCoding协议的对象),第二个法是异步执行,异步回调
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;

// 删--删除缓存
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;

@end

看了上面的代码后,使用那就是更加简单了

// 0.初始化YYCache
    YYCache *cache = [YYCache cacheWithName:@"myFirstDb"];
    // 1.缓存普通字符
    [cache setObject:@"缓存" forKey:@"savaKey"];
    NSString *name = (NSString *)[cache objectForKey:@"savaKey"];  //根据key取value
    NSLog(@"name: %@", name);
    // 2.缓存模型  (model需要遵循NSCoding协议)
    [cache setObject:model forKey:@"user"];

    // 异步缓存
    [cache setObject:array forKey:@"user" withBlock:^{
    }];
    //读取
    [cache objectForKey:@"user" withBlock:^(NSString * _Nonnull key, id<NSCoding>  _Nonnull object) {
         //读取后操作
     }];

本文总结:

由衷的佩服YYCache的作者, 真是太牛了,里面用到了很多技巧,本文就写到这里。YYCache git地址:点击打开链接

时间: 2024-10-07 06:47:24

iOS开发之缓存框架、内存缓存、磁盘缓存、NSCache、TMMemoryCache、PINMemoryCache、YYMemoryCache、TMDiskCache、PINDiskCache的相关文章

iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)

一. 概况 本文接着 iOS 开发之照片框架详解,侧重介绍在前文中简单介绍过的 PhotoKit 及其与 ALAssetLibrary 的差异,以及如何基于 PhotoKit 与 AlAssetLibrary 封装出通用的方法. 这里引用一下前文中对 PhotoKit 基本构成的介绍: PHAsset: 代表照片库中的一个资源,跟 ALAsset 类似,通过 PHAsset 可以获取和保存资源 PHFetchOptions: 获取资源时的参数,可以传 nil,即使用系统默认值 PHAssetCo

iOS 开发之照片框架详解之二 —— PhotoKit 详解(下)

这里接着前文<iOS 开发之照片框架详解之二 —— PhotoKit 详解(上)>,主要是干货环节,列举了如何基于 PhotoKit 与 AlAssetLibrary 封装出通用的方法. 三. 常用方法的封装 虽然 PhotoKit 的功能强大很多,但基于兼容 iOS 8.0 以下版本的考虑,暂时可能仍无法抛弃 ALAssetLibrary,这时候一个比较好的方案是基于 ALAssetLibrary 和 PhotoKit 封装出一系列模拟系统 Asset 类的自定义类,然后在其中封装好兼容 A

[转] iOS --- ReactiveCocoa - iOS开发的新框架

转载唐巧的博客:ReactiveCocoa - iOS开发的新框架

网络图片的获取以及二级缓存策略(Volley框架+内存LruCache+磁盘DiskLruCache)

在开发安卓应用中避免不了要使用到网络图片,获取网络图片很简单,但是需要付出一定的代价——流量.对于少数的图片而言问题不大,但如果手机应用中包含大量的图片,这势必会耗费用户的一定流量,如果我们不加以处理,每次打开应用都去网络获取图片,那么用户可就不乐意了,这里的处理就是指今天要讲的缓存策略(缓存层分为三层:内存层,磁盘层,网络层). 关于缓存层的工作,当我们第一次打开应用获取图片时,先到网络去下载图片,然后依次存入内存缓存,磁盘缓存,当我们再一次需要用到刚才下载的这张图片时,就不需要再重复的到网络

详细讲解Android的图片下载框架UniversialImageLoader之磁盘缓存(一)

沉浸在Android的开发世界中有一些年头的猴子们,估计都能够深深的体会到Android中的图片下载.展示.缓存一直是心中抹不去的痛.鄙人亦是如此.Ok,闲话不说,为了督促自己的学习,下面就逐一的挖掘Android中还算是比较牛叉的图片处理框架UniversialImageLoader以飨读者吧! 凡事如果过于草率必将陷入泥塘不能自拔.还是按部就班的一步一步的将这个框架给啃透. 第一个要讲的是磁盘的缓存的接口DiskCache 首先看一下其中的核心的接口的代码: File getDirector

IOS开发之异步加载网络图片并缓存本地实现瀑布流(二)

/* * @brief 图片加载通用函数 * @parma imageName 图片名 */ - (void)imageStartLoading:(NSString *)imageName{ NSURL *url = [NSURL URLWithString:imageName]; if([_fileUtil hasCachedImage:url]){ UIImageView *imageView = [[UIImageView alloc] init]; NSString *path = [_

详细讲解Android的图片下载框架UniversialImageLoader之磁盘缓存的扩展(二)

相对于第一篇来讲,这里讲的是磁盘缓存的延续.在这里我们主要是关注四个类,分别是DiskLruCache.LruDiskCache.StrictLineReader以及工具类Util. 接下来逐一的对它们进行剖析.废话不多说. 首先来看一下DiskLruCache. 这个类的主要功能是什么呢?我们先来看一段类的注释: /** * A cache that uses a bounded amount of space on a filesystem. Each cache * entry has a

iOS 开发之照片框架详解

一. 概要 在 iOS 设备中,照片和视频是相当重要的一部分.最近刚好在制作一个自定义的 iOS 图片选择器,顺便整理一下 iOS 中对照片框架的使用方法.在 iOS 8 出现之前,开发者只能使用 AssetsLibrary 框架来访问设备的照片库,这是一个有点跟不上 iOS 应用发展步伐以及代码设计原则但确实强大的框架,考虑到 iOS7 仍占有不少的渗透率,因此 AssetsLibrary 也是本文重点介绍的部分.而在 iOS8 出现之后,苹果提供了一个名为 PhotoKit 的框架,一个可以

ReactiveCocoa - iOS开发的新框架

本文转载至 http://www.infoq.com/cn/articles/reactivecocoa-ios-new-develop-framework ReactiveCocoa(其简称为RAC)是由Github 开源的一个应用于iOS和OS X开发的新框架.RAC具有函数式编程和响应式编程的特性.它主要吸取了.Net的 Reactive Extensions的设计和实现.本文将详细介绍该框架试图解决什么问题,以及其用法与特点. ReactiveCocoa试图解决什么问题 经过一段时间的研

iOS开发中的ARC内存管理de技术要点

本文旨在通过简明扼要的方式总结出iOS开发中ARC(Automatic Reference Counting,自动引用计数)内存管理技术的要点,所以不会涉及全部细节.这篇文章不是一篇标准的ARC使用教程,并假定读者已经对ARC有了一定了解和使用经验.详细的关于ARC的信息请参见苹果的官方文档与网上的其他教程:) 本文的主要内容: ARC的本质 ARC的开启与关闭 ARC的修饰符 ARC与Block ARC与Toll-Free Bridging ARC的本质 ARC是编译器(时)特性,而不是运行时