《Objective-C高级编程》温故知新之 自动引使用计数

作者 : 开心源码 本文共12502个字,预计阅读时间需要32分钟 发布时间: 2022-05-12 共70人阅读

本文”鸟瞰图”

前言

很久前看了《Objective-C高级编程 iOS与OS X多线程和内存管理》这本书,但当时看起来晦涩难懂。最近利使用下班时间重读了一遍,觉得还是得记录一下。毕竟每个阶段对相同的东西会有更深刻的了解。温故知新!

从自动引使用计数概念开始

概念:自动引使用计数是指内存管理中对内存管理中对引使用采取自动计数的计数。

工具:Clang是一个C语言、C++、Objective-C、Objective-C++语言的轻量级编
Clang用: clang -rewrite-objc (文件名)

说一下clang工具的用。比方我有一个类叫dwyane.m。里面代码如下:

int main(int argc, const char * argv[]) {    @autoreleasepool {                id __strong obj = [NSMutableArray array];    }    return 0;}id add(){    id __strong obj2 = [[NSMutableArray alloc] init];    return obj2;}

在终端,进入dwyane.m目录,clang -rewrite-objc dwyane.m ,而后,系统会为我们生成dwyane.cpp(C++文件),可以看到下列c++源码

int main(int argc, const char * argv[]) {    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;         id __attribute__((objc_ownership(strong))) obj = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("array"));    }    return 0;}id add(){    id __attribute__((objc_ownership(strong))) obj2 = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));    return obj2;}

Objective-C中的内存管理

也就是引使用计数。

文中利使用开关灯事件解释得非常完美。引使用数随着人员进屋离去随之加减。引使用数0时关灯

办公室的照明管理

转换到Objective-C程序中,其实就是下图

引使用计数的内存管理

内存管理的思考方式

  • 自己生成的对象,自己所持有。
  • 非自己生成的对象,自己也能持有。
  • 不再需要自己持有的对象时释放。
  • 非自己持有的对象无法释放。

上面出现的“生成” “持有” “释放” 再加上个 “废弃” 对应的OC方法如下

1、用allocnewcopymutableCopy的意味着自己生成的对象只有自己持有

eg:id obj = [NSObject alloc] init]; //自己生成并持有对象
其中自己可了解为“对象的用环境”或者者了解改变世界的程序员本身

2、使用allocnewcopymutableCopy外的方法获得的对象,因非自己生成并持有,so不是该对象的持有者。比方NSMutableArray类中的 array类方法

id obj = [NSMutableArray array]; //获得的对象存在,但自己不持有对象
retain可持有对象
[obj retain]; //这样跟上述的alloc等生成持有对象的方法就一样了。

3、自己持有的对象,不需要请使用release释放对象

id obj = [NSMutableArray array]; //获得的对象存在,但自己不持有对象
[obj release]; //释放对象
指向对象的指针依然被保留在变量obj中,貌似可访问,但对象一经释放,绝对不可访问。

命名规则:假如不是自己生成并持有的方法,不得使用allocnewcopymutableCopy开头的方法名。比方下面方法

id add(){    id  obj = [[NSObject alloc] init]; //自己生成并持有对象    [obj autorelease]; //释放,获得对象存在,但自己不持有对象    return obj;} 

autorelease使对象在超出指定的生存范围时能够自动并正确地释放(调使用release方法),如图

release 和 autorelease 的区别

4、无法释放非自己持有的对象,假如释放非自己持有的对象就会造成崩溃

alloc/retain/release/dealloc 实现

1、GNUstep的实现

因为NSObject类的源代码没有公开,所以借助与苹果的Cocoa框架相似的GNUstep来了解苹果的Cocoa实现。

GUNstep的中NSObject类的alloc类方法间接调使用NSZoneMalloc函数来分配存放对象所需的内存空间,之后将内存空间置0,最后返回作为对象而用的指针。

区域NSZoneMalloc的NSZone是什么呢?它是为防止内存碎片化而引入的结构。堆内存分配本身进行多重化管理,根据用对象的目的、对象的大小分配内存,从而提高了内存管理的效率。
但是现在运行时系统只是简单地忽略区域的概念。运行时系统中的内存管理本身已极具效率,用区域来管理内存反而会引起内存用效率低下以及源代码复杂化等问题。

image.png

alloc类方法使用struct obj_layout 中的 retain 整数来保存引使用计数,并将其写入内存头部,该对象内存块一律置0后返回。过程如图

对象的引使用计数可通过 retainCount 实例方法获得(非ARC下)

    id obj = [[NSObject alloc] init];    NSLog(@"retainCount=%lu", (unsigned long)[obj retainCount]);    /** 结果为 retainCount=1 */

由此可见,执行 alloc 后对象 retainCount 是 “1”。可以通过GNUstep的源代码确认一下

retainCount源代码

由对象寻址到对象内存头部,从而访问其中的 retained 变量。

通过对象访问对象内存头部

由于分配时一律置0,所以 retained 为0.由 NSExtaRefCount(self) + 1;得出,retainCount 为1.从而推测出,retain方法使 retained变量加1,而 release方法使 retained变量减1。

2、苹果的实现

alloc类方法首先调使用allocWithZone:类方法,这和GNUstep的实现相同,而后调使用class_createInstance 函数,最后通过调使用 calloc 来分配内存块。class_createInstance 函数的源码可以通过obj4库中的源码进行确认
从源代码的函数来看,苹果的实现大概就是采使用散列表(引使用计数表)来管理引使用计数。如图

GNUstep将引使用计数保存在对象占使用内存块头部的变量中,而苹果的实现,则是保存在引使用计数表中的记录中。

CGUstep的实现和苹果的实现好处区别如下:

通过内存块头部管理引使用计数的好处如下:

  • 一些代码即刻完成
  • 能够统一管理引使用计数使用内存块与对象使用内存块。

通过引使用计数表管理计数的好处如下:

  • 对象使用内存块的分配无需考虑内存块头部。
  • 引使用计数表各记录中存有内存块地址,可从各个记录追溯到各对象内存块。

其中第二条最重要。即便出现故障导致对象占使用的内存块损坏,但只需引使用计数表没有被破坏,就能够确认各内存块的位置。如图

另外,在利使用工具检测内存泄漏时,引使用计数表的各记录也有助于检测各对象的持有者能否存在。

autorelease

顾名思义,autorelease 就是自动释放,看起来像ARC,但实际上更相似C语言中的自动变量(局部变量)特性。
C语言的自动变量:程序执行时,某自动变量超过其作使用域,该自动变量将自动被废弃。

    {        int a;    } //超过变量 a 的作使用域,所以"()"外不可访问

区别在于 autorelease 可以被编程人员设定变量的作使用域。
autorelease 的具体用方法如下:

(1)生成并持有 NSAutoreleasePool 对象;
(2)调使用已分配对象的 autorelease 实例方法;
(3)废弃 NSAutoreleasePool 对象

使用代码来表示上图流程

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];    id obj = [[NSObject alloc] init];    [obj autorelease];    [pool drain]; //等同于 "[pool release]"

NSAutoreleasePool 对象的生存周期相当于C语言的作使用域。**对于所有调使用过 autorelease 实例方法的对象,在废弃 NSAutoreleasePool 对象时,都将调使用 release 实例方法。
在大量产生 autorelease 的对象时, 只需不放弃 NSAutoreleasePool 对象,那么生成的对象就不能释放,因而有时会产生内存不足现象。eg:读入大量图像的同时改变其尺寸。图像文件读入到 NSData 对象,并从中生成新的 UIImage 对象。这种情况下,就会大量产生 autorelease 的对象。

所以,需要在适当的地方生成、持有或者废弃 NSAutoreleasePool 对象。

所有权修饰符

  • __strong
  • __weak
  • __unsafe_unretained修饰符
  • __autoreleasing修饰符
“ __strong ” 修饰符

__strong 修饰符是id类型和对象类型默认的所有权修饰符。即下面等号左右两边相等
id obj = [NSObject alloc] init]; <==>id __strong obj = [NSObject alloc] init];

下面代码在ARC和非ARC状态下的样子
ARC:

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

非ARC:

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

可以看出,为了释放生成并持有的对象,添加了调使用release方法的代码。该源代码进行的动作同先前ARC有效时的动作完全一样。
如此源代码所示,__strong 修饰符修饰的变量obj在超出其变量作使用域时,即在该变量被废弃时,会释放其被赋予的对象。

{     /** obj0 持有对象A的强引使用 */    id __strong obj0 = [[NSObject alloc] init]; //对象A       /** obj1 持有对象B的强引使用 */    id __strong obj1 = [[NSObject alloc] init]; //对象B        /** obj2不持有对象 */    id __strong obj2 = nil;        /** obj0 持有赋值给obj2 的对象B的强引使用     *  同时obj0丢失原价对对象A的强引使用,即     *  对象A的所有者不存在,所以废弃对象A     *  此时,持有对象B的强引使用的变量为     *  obj0和obj1.     */    obj0 = obj1;        /** obj2持有由obj0赋值的对象B的强引使用     * 此时,持有对象B的强引使用的变量为     * obj0, obj1和obj2。     */    obj2 = obj0;        /** obj1对对象B的强引使用失效,此时     * 持有对象B的强引使用变量为 obj2.     */    obj1 = nil;        /** 对对象B的强引使用失效,对象B的所有者不存在,因而废弃对象B */    obj2 = nil;}

**__strong修饰符的变量,不仅只在变量作使用域,在赋值上也能够正确地管理其对象的所有者。

@implementation Test- (instancetype)init {    self = [super init];    return self;}- (void)setObject:(id __strong)obj {    obj_ = obj;}- (void)testMethod {    id __strong test = [[Test alloc] init]; //test生成并持有Test对象的强引使用    [test setObject:[[NSObject alloc] init]]; //Test对象的obj_成员持有NSObject对象的强引使用        /** 由于test变量超出其作使用域,强引使用失效,所以自动释放Test对象。Test对象的所以者不存在,所以废弃该对象。     * 废弃Test对象的同时,Test对象的成员obj_也被废弃,     *同时自动释放NSObject对象,NSObject对象的所有者不存在,所以废弃该对象 */}@end
“__weak ” 修饰符

看起来,苹果内存管理拥有__strong就足够,然而,不是这样的,遇到引使用计数式内存管理中必然会发生的“循环引使用”的问题,就需要使用到 __weak 修饰符了

循环引使用
我们修改下上面例子testMethod函数的代码。

- (void)testMethod {        id test0 = [[Test alloc] init]; //对象A    /** test0生成并持有Test对象A的强引使用 */        id test1 = [[Test alloc] init]; //对象B    /** test1生成并持有Test对象B的强引使用 */        [test0 setObject:test1]; //Test对象的obj_成员持有赋值给test1的Test对象B的强引使用    /** 此时,持有Test对象B的变量有     * 对象A的obj_成员以及test1 */         [test1 setObject:test0]; //Test对象的obj_成员持有赋值给test1的Test对象的强引使用    /** 此时,持有Test对象A的变量有     * 对象B的obj_成员以及test0 */}


循环引使用容易发生内存泄漏,所谓内存泄漏就是应当废弃对象在超出其生命周期后继续存在。
还有,只有一个对象,其持有其自身,也会内存泄漏。

    id test = [[Test alloc] init]; //生成并持有NSObject对象    [test setObject:test]; //NSObject对象被NSObject的obj_成员强引使用

如图

接下来利使用__weak修饰符处理循环问题,再修改上面例子

{    id __strong obj0 = [[NSObject alloc] init]; //obj0生成并持有NSObject对象的强引使用    id __weak obj1 = obj0;    /** obj1 变量持有 NSObject 的弱引使用 */}/** 由于obj0 变量超出其作使用域,强引使用失效,所以自动释放自己持有NSObject对象*  又由于__weak修饰符的变量(即弱引使用)不持有对象*  对象持有者一律不存在,所以被废弃

如图

__weak修饰符还有个优点:持有某对象弱引使用时,若该对象被废弃,则此弱引使用将自动失效且处于nil被赋值的状态(空弱引使用)

“__unsafe_unretained”修饰符

__weak 修饰符只能使用于iOS5以上及OS X Lion以上版本的应使用程序,在iOS4以及OS X Snow Leopard 的应使用程序可用 __unsafe_unretained 代替

__unsafe_unretained 修饰符是不安全的修饰符,虽然ARC式的内存管理是编译器的工作,但附有__unsafe_unretained 修饰符的变量不属于编译器的内存管理对象。

“__autoreleasing 修饰符”

ARC有效时,使用@autoreleasepool 块替代 非ARC的 NSAutoreleasePool 类,使用附有 __autoreleasing 修饰符的变量替代autoreleasing 方法。如图


注意:但是,显式地附加 __autoreleasing 修饰符同显式地附加 __strong 修饰符一样罕见。这是由于编译器会检查方法名能否以alloc/new/copy/utableCopy开始,假如不是则自动将返回值的对象注册到 autoreleasepool。比方

+ (id) array{    id obj = [[NSMutableArray alloc] init];    return obj;}

<上述代码没有显示用__autoreleasing 修饰符,但是与不附加,结果完全一样,由于,return使得obj对象超出其作使用域,所以该强应使用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器会自动将其注册到 autoreleasepool中。
而,在访问附有 __weak 修饰符的变量时,实际上必定要访问注册到autoreleasepool的对象。为什么?请先看下列代码

    id __weak obj1 = obj0;    NSLog(@"class = %@", [obj1 class]);

以下源代码与此相同

    id __weak obj1 = obj0;    id __autoreleasing tmp = obj1;    NSLog(@"class = %@", [tmp class]);

由于__weak 修饰符只持有对象的弱引使用,而在访问引使用对象的过程中,该对象有可能被废弃。假如把要访问的对象注册到 autoreleasepool 中,那么在autoreleasepool块结束前都能确保变量存在。

**注意:最后一个可非显示 __autoreleasing 修饰符的例子,id *obj 我们可能会类推出 id __strong *obj,但结果却是 id __autoreleasing *obj。同样,NSObject **obj 则是 NSObject *__autoreleasing *obj。

ARC规则

  • 不能用 retain/release/retain/autorelease
  • 不能用NSAllocateObject/NSDeallocateObject
  • 须遵守内存管理的方法命名规则
  • 不要显示调使用dealloc
  • 用@autoreleasepool 块替代 NSAutoreleasePool
  • 不能用区域(NSZone)
  • 对象型变量不能作为C语言结构体(struct/union)的成员
  • 显示转换“id”和“void”

不要显示调使用dealloc

- (void)dealloc {    [super dealloc];}/** 这样会报错 */

对象型变量不能作为C语言结构体(struct/union)的成员

struct Data {    NSMutableArray *array;};/** error:ARC forbids Objective-C objects in struct */

假如肯定要把对象型变量加入到结构体成员中,可强制转换为 void * 或者者附加 __unsafe_unretained修饰符

struct Data {    NSMutableArray __unsafe_unretained *array;};

显示转换“id”和“void”

非ARC

    id obj = [[NSObject alloc] init];    void *p = obj;

ARC下
则会报错

Implicit conversion of Objective-C pointer type ‘id’ to C pointer type ‘void *’ requires a bridged cast

错误提醒了我们,可使用bridge,我们修改下代码就可,如下:

    id obj = [[NSObject alloc] init];    void *p = (__bridge void *)obj; //id 转 void *    id o = (__bridge id)p; //void * 转 o

注意:前关注下Objective-C 对象与 Core Foundation 对象的互换以及免费桥
(Toll-Free Bridge)的用

__bridge_retained 和 __bridge_transfer转换

__bridge_retained

        /** ARC: */                id obj = [[NSObject alloc] init];        void *p = (__bridge_retained void *)obj; //等同于下面         /** 非ARC */         // __bridge_retained转变成了retain。变量obj和变量p同时持有对象。        id obj = [[NSObject alloc] init];        void *p = obj;        [(id)p retain];

__bridge_transfer

        /** ARC */        void *p = (__bridge_retained void *)[[NSObject alloc] init];        NSLog(@"class=%@", [(__bridge id)p class]);        (void)(__bridge_transfer id)p; //释放了p,跟[p release];相同    //等同于下面        /** 非ARC */        id p = [[NSObject alloc] init];        NSLog(@"class=%@", [p class]);        [p release]; 

Objective-C对象与 Core Foundation 对象

Core Foundation对象主要用在使用C语言编写的Core Foundation框架中,并用引使用计数的对象。在ARC无效时,CF的CFRetain/CFRelease对应retain/release
CF 对象和OC对象没有区别,所以在ARC无效时,使用简单的C语言转换也能实现互换。另外这种互换不需要用额外的CPU资源,因而被称为免费桥。

1、OC转CF

        //可使用于toll-free bridge的互换        CFMutableArrayRef cfObject = NULL;        id obj12 = nil;        {            //obj持有对象A的强引使用            id obj = [[NSMutableArray alloc] init]; //对象A            //cfObject也持有对象A的强引使用//            cfObject = (__bridge_retained CFMutableArrayRef)obj; //  等同于          cfObject = CFBridgingRetain(obj);            //注意: __bridge 不会对引使用计数产生影响            cfObject = (__bridge CFMutableArrayRef)obj;//            obj12 = obj; //obj2也持有对象A的强引使用            CFShow(cfObject);            printf("reain count = %ld\n", CFGetRetainCount(cfObject));             /** 打印:reain count = 1 */        }        //下面访问对象出错--》 出现悬垂指针        printf("retain count after the scope = %ld\n", CFGetRetainCount(cfObject)); //对象的引使用技术 :引使用计数就是对一个对象记录其被引使用的次数,其的引使用计数可加可减

悬垂指针 :指向曾经存在的对象,但该对象已经不再存在了,此类指针称为悬垂指针。结果未定义,往往导致程序错误,而且难以检测。

2、CF转OC

            //生成并持有对象        CFMutableArrayRef cfObject = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL);        printf("retain count = %ld\n", CFGetRetainCount(cfObject));         /** 打印:retain count = 1 */        //通过CFBridgingRelease赋值,变量obj持有对象强引使用的同时,对象调使用CFRelease释放,相当于调使用了(__bridge_transfer id)cfObject        id obj = CFBridgingRelease(cfObject);  //After using a CFBridgingRetain on an NSObject, the caller must take responsibility for calling CFRelease at an appropriate time.//        id obj = (__bridge_transfer id)cfObject;  //(__bridge_transfer id)X        //cfObject上面已经被释放,你会奇怪为什么还有,这不是悬垂指针吗?其实不是,由于0bj继续持有对对象的强引使用,所以cfObject也指向依然存在的对象,可以正常用        printf("retain count after the cast = %ld\n", CFGetRetainCount(cfObject));        /** 打印:retain count after the cast = 1 */

属性

ARC有效时,以下可作为属性公告中用的属性来使用。

书原文中写道:在公告类成员变量时,假如同属性公告中的属性不一致则会引起编译错误。比方

@property (nonatomic, weak) id obj1;

需要改成

@property (nonatomic, weak) id __weak obj1;

又或者者把属性公告改成strong

@property (nonatomic, strong) id obj1;

但经笔者实验,在Xcode V9.2 、macOS 10.12.6 下编译运行成功,并无报错

数组

id __strong *array = nil;

注意:id *类型 默认为”id __autoreleasing *“类型,所以需要显式指定为__strong修饰符。另外,上式尽管保证了附有__strong修饰符的id型变量被初始化为nil,但并不能保证附有__strong修饰符的id指针型变量被初始化为nil。

在动态数组中操作附有__strong修饰符的变量与静态数组有很大差异,需要自己释放所有的元素。如下源码,在只是简单地使用free函数废弃了数组使用内存块的情况下,数组各元素所赋值的对象不能再次释放,从而引起内存泄漏。
free(array)

这是由于在静态数组中,编译器能够根据变量的作使用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从解决。所以肯定要将对象赋值nil,使元素所赋值对象强引使用失效,从而释放对象,再free函数废弃内存块

    for (NSUInteger i = 0; i < entries; ++i) { //entries为分配了所需内存块的个数        array[i] = nil;        free(array);

ARC的实现

1、__strong修饰符的实现

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

上面代码如何运行呢?看看汇编和苹果源码obj4库,大概知道程序是如何工作的。下面请看编译器的模拟源代码

由图可知,2次调使用了obj_msgSeng 方法(alloc 和 init 方法),变量作使用域结束时通过 objc_release 释放对象(编译器自动插入了release)

_objc_retainAutoreleasedReturnValue函数主要使用于最优化程序运行。顾名思义,它是使用于自己持有(retain)对象的函数,但它持有的对象应为返回注册在autoreleasepool中对象的方法,或者者是函数的返回值。
_objc_autoreleaseReturnValue与之相对应,使用于NSMutableArray类的array类方法等返回对象的实现上。

注意:_objc_autoreleaseReturnValue函数会检查用该函数的方法或者函数调使用方的执行命令列表,假如方法或者函数的调使用方在调使用了方法或者函数后紧接着调使用_objc_retainAutoreleasedReturnValue()函数,那么就不将返回的对象注册到autoreleasepool中,而是直接传递到方法或者函数的的调使用方。_objc_retainAutoreleasedReturnValue()函数与obj_retain函数不同,它即使不注册到autoreleasepool中而返回对象,也能正确的获取对象。

2、__weak 修饰符的实现

1、若附有__weak修饰符的变量所引使用的对象被废弃,则将nil赋值给改变量。
2、用附有__weak修饰符的变量,即是用注册到autoreasepool中的对象。

那他们是如何实现的呢?请看下列代码

    {        id __weak obj1 = obj;    }

下面请看编译器的模拟源代码

那具体如何实现上图的操作,请继续看源码

objc_storeWeak 函数把第二参数的赋值对象的地址作为建值,将第一参数的附有__weak修饰符的变量的地址注册到 weak 表中。假如第二参数为0,则将变量的地址从weak 表从 weak 表中删除。

weak 表与引使用计数表相同,作为散列表被实现。假如大量用附有 __weak 修饰符的变量,则会消耗相应的 CPU 资源。良策是只在需要避免循环引使用时才用 __weak 修饰符

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

但上面会引起编译器警告,由于__weak修饰,NSObject 没有所有者,创立后,马上就通过 objc_release 函数被废弃。

我们看下下列代码,验证功能
用附有__weak修饰符的变量,即是用注册到autoreasepool中的对象。

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

源代码如下:

由此可知,由于附有__weak修饰符变量所引使用的对象像这样被注册到autoreleasepool中,所以在@autoreleasepool块结束前之前都可以放心用。但大量用__weak修饰的变量,

注册到autoreleasepool的对象也会大量添加,最好先暂时赋值给__strong修饰符的变量后再用。

        id obj = [[NSObject alloc] init];        id __weak obj1 = obj;        NSLog(@"1 = %@", obj1);        NSLog(@"2 = %@", obj1);        NSLog(@"3 = %@", obj1);        NSLog(@"4 = %@", obj1);

上面变量obj所赋值的对象也就注册到autoreleasepool4次

建议用:

        id obj = [[NSObject alloc] init];        id __weak obj1 = obj;        id tmp = obj1;        NSLog(@"1 = %@", tmp);        NSLog(@"2 = %@", tmp);        NSLog(@"3 = %@", tmp);        NSLog(@"4 = %@", tmp);

相关文献:
http://www.cocoachina.com/ios/20150610/12093.html
免费桥(Toll-free bridge)
How does objc_retainAutoreleasedReturnValue work?

上一篇 目录 已是最后

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 《Objective-C高级编程》温故知新之 自动引使用计数

发表回复