美文网首页
iOS开发读书笔记:Objective-C高级编程 iOS与OS

iOS开发读书笔记:Objective-C高级编程 iOS与OS

作者: Ryan___ | 来源:发表于2018-08-29 22:35 被阅读34次

    iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-上篇(自动引用计数)
    iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-中篇(Blocks)
    iOS开发读书笔记:Objective-C高级编程 iOS与OS X多线程和内存管理-下篇(GCD)

    阅读完Block此篇后,可以与iOS开发经验(25)-Block一块阅读,主要是可以加深对__forwarding的理解。

    目录

    • 2.1 Blocks概要
      • 2.1.1 什么是Blocks
    • 2.2 Blocks模式
      • 2.2.1 Blocks语法
      • 2.2.2 Blocks类型变量
      • 2.2.3 截获自动变量值
      • 2.2.4 __block说明符
      • 2.2.5 截获的自动变量
    • 2.3 Blocks的实现
      • 2.3.1 Block的实质
      • 2.3.2 截获自动变量值
      • 2.3.3 __block说明符
      • 2.3.4 Block存储域
      • 2.3.5 __block变量存储域
      • 2.3.6 截获对象
      • 2.3.7 __block变量和对象
      • 2.3.8 Block循环引用
      • 2.3.9 copy/release

    2.1 Blocks概要

    2.1.1 什么是Blocks

    Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
    顾名思义,所谓匿名函数就是不带有名称的函数。
    C语言的标准函数如下:

    int func(int count);//声明函数
    int result = func(10);//调用函数
    

    如果像下面这样,使用函数指针来代替直接调用函数,必须使用该函数的名称func。

    int result = (*funcptr)(10);
    

    这样以来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
    但其实使用函数指针也仍然需要知道函数名称。若不使用想赋值的函数的名称,就无法取得该函数的地址。

    int (*funcptr)(int) = &func;
    int result = (*funcptr)(10);
    

    通过Blocks,源代码中就能够使用匿名函数,而不带名称的函数。
    到这里我们知道了"带有自动变量值的匿名函数"中"匿名函数"的概念。那么“带有自动变量值”究竟是什么呢?
    首先回顾下函数中可能使用的变量:

    • 自定变量(局部变量)
    • 函数的参数
    • 静态变量(静态局部变量)
    • 静态全局变量
    • 全局变量

    虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。
    另外,“带有自动变量值的匿名函数”这一概念并不仅指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包。

    2.2 Blocks模式

    2.2.1 Blocks语法

    与一般的函数定义相比,仅有两点不同

    • 没有函数名
    • 带有“^”记号(插入记号):因为OS X、iOS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。
      以下为Block语法的BN范式
    ^ 返回值类型 参数列表 表达式
    
    ^ int (int count) {
        return count + 1;
    }
    
    该源代码可省略胃如下形式
    ^ {
        return count + 1;
    }
    

    2.2.2 Blocks类型变量

    在Block语法下,可将Block语法赋值给声明为Block类型的变量中。既源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。

    int (^blk)(int);
    

    与前面的使用函数指针的源代码对比可知,声明Block类型变量仅仅是将声明函数指针类型变量的“*”变为“^”。该Blcok类型变量与一般的C语言变量完全相同,可作为以下用途使用 :

    • 自定变量(局部变量)
    • 函数的参数
    • 静态变量(静态局部变量)
    • 静态全局变量
    • 全局变量

    那么,下面我们就试着使用Block语法将Block赋值为Block类型变量:

    int (^blk) (int) = ^ (int count) {
        return count + 1;
    }
    
    也可以:
    
    int (^blk1)(int) = blk;
    

    但是此记述方式极为复杂。这时,我们可以像使用函数指针类型时那样,使用typedef来解决问题。

    typedef int (^blk_t) (int);
    

    如上所示,通过使用typedef可声明“blk_t”类型变量。这样函数定义就变得更容易理解了。
    另外,将赋值给Block类型变量中的Block方法像C语言通常的函数调用那样使用,这种方法与使用函数指针类型变量调用函数的方法几乎完全相同。

    2.2.3 截获自动变量值

    通过以上说明,我们已经理解了“带有自动变量值的匿名函数”中的“匿名函数”。而“带有自动变量值”究竟是什么呢?“带有自动变量值”在Block中表现为“截获自动变量值”。截获自动变量值的实例如下:

    int main() {
        int val = 10;
        void (^blk)(void) = ^ {
             printf(val);
        }
        val = 2;
        blk();
        //打印结果为10;
    }
    

    Block中,Block表达式截获所使用的自动变量的值,既保持该自动变量的瞬间值。这就是自动变量值的截获。

    2.2.4 __block说明符

    实际上,自动变量值截获指南保持秩序Block语法瞬间的值。保存后就不能改写该值。如果尝试在Block中改写截获的自动变量值,会发生编译错误。
    若想在Block语法的表达式中将值赋值在Block语法外声明的自动变量,需要在该自动变量上添加__block说明符。该变量称为__block变量。

    int main() {
        __block int val = 10;
        void (^blk)(void) = ^ {
            val = 1;
        }
        val = 2;
        blk();
        //打印结果为10;
    }
    

    2.2.5 截获的自动变量

    截获OC对象,调用变更该对象的方法不会产生编译错误,而向截获的变量array赋值则会产生编译错误。

    //编译正常
    id array = [[NSMutableArray alloc] init];
    void (^blk) (void) = ^ {
        id obj  = [[NSObject alloc] init];
        [array addObject:obj];
    }
    
    //编译错误
    id array = [[NSMutableArray alloc] init];
    void (^blk) (void) = ^ {
        array = [[NSMutableArray alloc] init];
    }
    

    以上的第二段代码需要给截获的自动变量附加__block说明符。

    2.3 Blocks的实现

    2.3.1 Block的实质

    Block上“带有自动变值的匿名函数”,但Block究竟是什么呢?
    它实际上是作为极普通的C语言源代码来处理的,通过支持Block的编译器,含有Block的编译器,含有Block语法的源代码转换为一般C语言编译器能够处理的源代码,并作为极普通的C语言源代码被编译。
    clang(LLVM编译器)具有转换为我们可读源代码的功能。通过“-rewrite-objc”选项就能将含有Block语法的源代码变换为C++的源代码。

    clang -rewrite-objc xxx.m
    

    其实,所谓Block就是Objective-C对象。Block指针赋值给Block的结构体成员变量isa。

    struct _main_block_impl_0 {
        void *isa;   
        int flags;
        int Reserved; 
        void *FuncPtr;   
    };
    

    此_main_block_impl_0结构体相当于基于objc_object结构体的Objective-C类对象的结构体。另外,对其中的成员变量isa进行初始化,具体如下:

    isa = &_NSConcreteStackBlock;
    

    既_NSConcreteStackBlock相当于class_t结构体实例。在将Block作为Objective-C的对象处理时,关于该类的信息放置于_NSConcreteStackBlock中;

    2.3.2 截获自动变量值

    本节主要讲解如何截获自动变量值。将截获自动值的源代码用过clang进行转换(源代码略)。
    我们注意到,Block语法表达式中使用的自动变量被作为成员变量追加到了_main_block_impl_0结构体中。

    struct _main_block_impl_0 {
        struct __block_impl impl;   
        struct __main_block_desc_0 *Desc;   
        int var; 
    };
    

    _main_block_impl_0结构体内声明的成员变量类型与自动变量类型完全相同。请注意,Block语法表达式中没有使用的自动变量不会被追加。Block的自动变量截获只针对Block中使用的自动变量。
    总的来说,所谓“截获自动变量值”意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block的结构体实例(既Block自身)中。

    2.3.3 __block说明符

    以上的截获自动变量的代码例子,在Block的结构体实例中重写该自动变量也不会改变原先截获的自动变量。因为在实现上不能改写被截获自动变量的值,所以会发生编译错误。
    不过这样以来,无法在Block中保存值了,极为不便。但是有两个方法:

    1. 有如下几个变量,允许Block改写值:
      • 静态变量
      • 静态全局变量
      • 全局变量
    2. 使用__block修饰变量 :__block 存储域类说明符

    C语言有如下存储域类说明符:

    1. typedef
    2. extern
    3. static:表示作为静态变量存在在数据区中
    4. auto:表示作为自动变量存储在栈中
    5. register

    __block说明符类似于static、auto和register说明符,它们用于指定将变量值设置到哪个存储域中。

    个人笔记

    2.3.4 Block存储域

    通过前面说明可知,Block转换为Block的结构体类型的自动变量,__block变量转换为__block变量的结构体类型的自动变量。所谓结构体类型的自动变量,既栈上生成的该结构体的实例。
    另外通过之前的说明可知Block也是Objective-C对象,该Block的类为_NSConcreteStackBlock。有很多与之类似的类,如:

    • _NSConcreteStackBlock,既该类的对象Block设置在栈上
    • _NSConcreteGlobalBlock,设置在程序的数据区域(.data)中
    • _NSConcreteMallocBlock,设置在由malloc函数分配的内存块(既堆)中

    在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreteGlobalBlock类对象。例如

    void (^blk)(void) = ^ {
        printf("Global Block");
    }
    

    此源代码通过声明全局变量blk来使用Block语法。如果转换该源代码,Block用结构体的成员变量isa的初始化如下:

    impl.isa = & _NSConcreteGlobalBlock;
    

    该Block的类为_NSConcreteGlobalBlock类。此Block既该Block用结构体实例设置在程序的数据区域中。
    在以下情况下,Block为_NSConcreteGlobalBlock类对象

    • 记述全局变量的地方有Block语法时
    • Block语法的表达式中不使用应截获的自动变量时

    除此之外的Block语法生成的Block为_NSConcreteStackBlock类对象,且设置在栈上。
    配置在全局变量上的Block,从变量作用域外也可以通过指针安全的使用。但设置在栈上的Block,如果其所属的变量作用域结束,该Block就被废弃。由于__block变量也配置在栈上,同样的,如果其所属的变量作用域结束,则该__block变量也会被废弃。
    Block提供了将Block和__block变量从栈上复制到堆上的方法来解决这个问题。将配置在栈上的Block复制到堆上,这样即使Block语法记述的变量作用域结束,堆上的Block还可以继续存在。
    复制到堆上的Block将_NSConcreteMallocBlock类对象写入Block用结构体实例的成员变量isa。

    impl.isa = & _NSConcreteMallocBlock;
    

    而__block变量用结构体成员变量_forwarding可以实现无论__block变量配置在栈上还是堆上时都能够正确的访问__block变量。在此情形下,只要栈上的结构体实例成员变量__forwarding指向堆上的结构体实例,那么不管是从栈上的__block变量还是从堆上的__block变量都能够正确的访问。

    那么Block提供的复制方法是什么呢?当ARC时,大多数情形下编译器会恰当的判断,自动生成将Block从栈上复制到堆上的代码。
    当Block作为函数返回值返回时,执行objc_retainBlock方法,实际上是copy函数。
    那么少数情形有几种呢?

    1. XXXX

    另外,对于已配置在堆上的Block以及配置在程序的数据区域的Block,调用copy方法又会如何呢?

    • _NSConcreteMallocBlock:引用计数增加
    • _NSConcreteStackBlock:从栈复制到堆
    • _NSConcreteGlobalBlock:什么也不做

    不管是Block配置在何处,用copy方法复制都不会引起任何问题。在不确定时调用copy方法即可

    2.3.5 __block变量存储域

    上节只对Block的copy进行了说明,使用__block变量的Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上。这些__block变量也全部从栈复制到堆。此时,Block持有__block变量。
    如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放。
    此思考方式与OC的引用计数内存管理完全相同。使用__block变量的Block持有__block变量。日光Block被废弃,它所持有的__block变量也就被释放。

    那么在理解了__block变量的存储域之后,在回顾下之前讲过的使用__block变量用结构体成员变量__forwarding的原因。“不管__block变量配置在栈上还是在堆上,都能够正确的访问该变量”。正如这句话所述,通过Block的复制,__block变量也从栈复制到堆。此时可同时访问栈上的__block变量和堆上的__block变量。
    源代码可转换为如下形式:

    (val.__forwarding->val);
    

    在变换Block语法的函数中,该变量val为复制到堆上的__block变量用结构体实例,而使用的与Block无关的变量val,为复制前栈上的__block变量用结构体实例。
    但是栈上的__block变量用结构体实例在__block变量从栈复制带堆上时,会将成员变量__forwarding的值替换为复制目标堆上的__block变量用结构体实例的地址。
    通过该功能,无论上在Block语法中、block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利的访问同一个__block变量。

    2.3.6 截获对象

    以下源代码生成并持有NSMutableArray类的对象,由于附有__strong修饰符的赋值目标变量的作用域立即结束,因此对象被立即释放并废弃。

    {
        id array = [[NSMutableArray alloc] init];
    }
    

    下面我们看一下在Block语法中使用该变量array的代码:

    //运行正常
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = [^(id obj) {
            [array addObject:obj];
        } copy];
    }
    
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    

    该代码运行正常,执行结果如下

    array count = 1;
    array count = 2;
    array count = 3;
    

    请注意被赋值NSMutableArray类对象并被截获的自动变量array。我们可以发现它是Block用的结构体中附有__strong修饰符的成员变量。

    struct _main_block_impl_0 {
        struct __block_impl impl;   
        struct __main_block_desc_0 *Desc;   
        id __strong array; 
    };
    

    在OC中,C语言结构体不能含有附有__strong修饰的变量。因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存。

    但是OC的运行时库能够很准确把握Block从栈复制到堆以及堆上的Block被废弃的时机,因此Block用结构体中即使含有附有__strong修饰符或__weak修饰符的变量,也可以恰当的进行初始化和废弃。为此需要使用在__main_block_desc_0结构体中增加的成员变量copy和dispose,以及作为指针赋值给该成员变量的_main_block_copy_0函数和_main_block_dispose_0函数。
    恰当管理赋值给变量array的对象:__main_block_copy_0函数使用_Block_object_assign函数将对象类型对象复制给Block用结构体的成员变量array中并持有该对象。

    _Block_object_assign函数调用相当于retain实例方法的函数,将对象赋值在对象类型的结构体成员变量中。

    另外,__main_block_dispose_0函数使用_Block_object_dispose函数,释放赋值在Block用结构体成员变量array中的对象。
    _Block_object_dispose函数调用相当于release实例方法的函数,释放赋值在对象类型的结构体成员变量中的对象。

    虽然此__main_block_copy_0函数(以下简称copy函数)和__main_block_dispose_0函数(以下简称dispose函数)指针被赋值在__main_block_desc_0结构体成员变量copy和dispose。在Block从栈复制到堆时以及堆上的Block被废弃时会调用这些函数。

    • copy函数:栈上的Block复制到堆时;
    • dispose函数:堆上的Block被废弃时;

    那么什么时候栈上的Block会复制到堆呢?

    • 调用Block的copy实例方法时;
    • Block作为函数返回值返回时;
    • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时;
    • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时;

    在上面这些情况下栈上的Block赋值到堆上,其实可归结为_Block_copy函数被调用时Block从栈复制到堆。相对的,在释放复制到堆上的Block后,谁都不持有Block而使其被废弃时调用dispose函数。这相当于对象的dealloc实例方法。
    有了这种构造,通过使用附有__strong修饰符的自动变量,因而Block中截获的对象就能够超出其变量作用域而存在。

    2.3.7 __block变量和对象

    __block说明符可指定任何类型的自动变量。

    __block id obj = [[NSObject alloc] init];
    

    其实该代码等同于

    __block id __strong obj = [[NSObject alloc] init];
    

    ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量。
    在Block中使用附有__strong修饰符的id类型或对象类型自动变量的情况下,当Block从栈复制到堆时,使用Block_object_assign函数,持有Block截获的对象。当堆上的Block被废弃时,使用_block_object_dispose函数,释放Block截获的对象。
    在__block变量为附有_strong修饰符的id类型或对象类型自动变量的情形下会发生同样的过程。当__block变量从栈复制到堆时,使用_Block_object_assign函数,持有赋值给__block变量的对象。当堆上的__block变量被废弃时,使用_Block_object_dispose函数,释放赋值给__block变量的对象。
    由此可知,即使对象赋值复制到堆上的附有_strong修饰符的对象类型__block变量中,只要__block变量在堆上继续存在,那么该对象就会继续处于被持有的状态。这与Block中使用赋值给附有__strong修饰符的对象类型自动变量的对象相同。

    另外,我们前面用到的只有附有__strong修饰符的id类型或对象类型自动变量。如果使用__weak修饰符会如何呢?首先是在Block中使用附有__weak修饰符的id类型变量的情况。

    blk_t blk;
    {
        id array = [[[NSMutableArray alloc] init];
        id __weak array2 = array;
        blk = [^(id obj) {
            [array2 addObject:obj];
        } copy];
    }
    
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    

    该代码可正常执行。 执行结果,这与以上代码的结果不同:

    array2 count = 0;
    array2 count = 0;
    array2 count = 0;
    

    这是由于附有__strong修饰符的变量array在该变量作用域结束的同时被释放、废弃,nil被赋值在附有__weak修饰符的变量array2中。

    2.3.8 Block循环引用

    如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环利用。我们来看看下面的源代码:

    typedef void (^blk_t)(void);
    
    @interface MOyObject : NSObject
    {
          blk_t blk_;
    }
    @end
    
    @implementation MyObject
    
    - (id)init {
        self = [super init];
        blk_ = ^ {
            NSLog(@"self = %@",self);
        } ;
        return self;
    }
    
    - (void)dealloc {
        NSLog(@:dealloc:);
    }
    @end
    
    int main() {
        id o = [[MyObject alloc] init];
        NSLog(@"%@",o);
       return 0;
    }
    

    该源代码中MyObject类的dealloc实例方法一定没有被调用。
    MyObject类对象的Block类型成员变量blk_持有赋值为Block的强引用。既MyObject类对象持有Nlock。init实例方法中执行的Block语法使用附有_strong修饰符的id类型变量self。并且由于Block语法赋值在了成员变量blk中,因此通过Block语法生成在栈上的Block此时由栈复制到堆,并持有所使用的self。self持有Block,Block持有self。这正是循环引用。
    另外,编译器在编译该源代码是能够查处循环引用,因此编译器能正确的进行警告。
    为避免此循环引用,可声明附有__weak修饰符的变量,并将self赋值使用。

    - (id)init {
        self = [super init];
        id __weak tmp = self;
        blk_ = ^ {
            NSLog(@"self = %@",tmp);
        } ;
        return self;
    }
    

    在该源代码中,由于Block存在时,持有该Block的MyObject类对象赋值在变量tmp中的self必须存在,因此不需要判断tmp的值是否为nil。
    在面相iOS4(MRC),必须使用__unsafe_unretained修饰符代替__weak修饰符。在此源代码中也可使用__unsafe_unretained修饰符,且不必担心悬挂指针。

    - (id)init {
        self = [super init];
        id __unsafe_unretained tmp = self;
        blk_ = ^ {
            NSLog(@"self = %@",tmp);
        } ;
        return self;
    }
    

    另外在以下源代码中Block内没有使用self也同样截获了self,引起了循环引用。

    typedef void (^blk_t)(void);
    
    @interface MOyObject : NSObject
    {
          blk_t blk_;
          id obj_;
    }
    @end
    
    @implementation MyObject
    
    - (id)init {
        self = [super init];
        blk_ = ^ {
            NSLog(@"obj_ = %@",obj_);
        } ;
        return self;
    }
    

    既Block语法内使用的obj_实际上截获了self。对编译器来说,obj_只不过是对象用结构体的成员变量。

    blk_ = ^ {
        NSLog(@"obj_ = %@",self->obj_);
    };
    

    该源代码也基本与前面一样,声明附有_weak修饰符的变量并赋值obj使用来避免循环引用。在此源代码中也可安全的使用__unsafe_unretained修饰符,原因同上。

    - (id)init {
        self = [super init];
        id __weak obj = obj_;
        blk_ = ^ {
            NSLog(@"obj = %@",obj);
        } ;
        return self;
    }
    

    在为避免循环引用而使用__weak修饰符时,虽说可以确认使用附有__weak修饰符的变量时是否为nil,但更有必要使之生成以使用赋值给附有__weak修饰符变量的对象。
    另外,还可以使用__block变量来避免循环引用。

    typedef void (^blk_t)(void);
    
    @interface MOyObject : NSObject
    {
          blk_t blk_;
    }
    @end
    
    @implementation MyObject
    
    - (id)init {
        self = [super init];
        __block id tmp = self;
        blk_ = ^ {
            NSLog(@"self = %@",tmp);
            tmp = nil;
        } ;
        return self;
    }
    
    - (void)execBlock {
        blk();
    }
    
    - (void)dealloc {
        NSLog(@:dealloc:);
    }
    
    @end
    
    int main() {
        id o = [[MyObject alloc] init];
        [o execBlock];
       return 0;
    }
    

    该源代码没有循环引用。原因:通过执行execBlock实例方法,Block被实行,nil被赋值在__block变量tmp中。因此,_block变量tmp对MyObject类对象的强引用失效。但是如果不调用execBlock实例方法,既不执行赋值给成员变量blk的Block,便会循环引用并引起内存泄漏。
    在生成并持有MyObject类对象的状态下会引起以下循环引用:

    • MyObject类对象持有Block;
    • Block持有__block变量;
    • __block变量持有MyObject类对象;

    下面我们对使用__block变量避免循环引用的方法和使用__weak 修饰符及__unsafe_unretained修饰符避免循环引用的方法做个比较。
    使用__block变量的优点如下:

    • 通过__block变量可控制对象的持有期间
    • 在不能使用__weak修饰符的环境中不使用__unsafe_unretained修饰符即可(不必担心悬垂指针)

    在执行Block时可动态的决定是否将nil或其他对象赋值在__block变量中。

    使用__block变量的缺点如下:

    • 为避免循环引用必须执行Block

    存在执行了Block语法,却不执行Block的路径时,无法避免循环引用。若有雨Block引发了循环引用时,根据Block的用途选择使用__block变量、__weak修饰符或__unsafe_unretained修饰符来避免循环引用。

    2.3.9 copy/release

    ARC无效时,一般需要手动将Block从栈复制到堆。另外,由于ARC无效,所以肯定要释放赋值的Block。这时我们用copy实例方法用来赋值,用release实例方法来释放。

        [blk_ release];
    

    只要Block有一次复制并配置在堆上,就可通过retain实例方法持有。

        [blk_ retain];
    

    但是对于配置在栈上的Block调用retain实例方法则不起任何作用。

        [blk_ retain];
    

    该源代码中,虽然堆赋值给blk_的栈上的Block调用了retain实例方法,但实际上对此源代码不起任何作用。因此推荐使用copy实例方法来持有Block。

    另外,ARC无效时,__block说明符被用来避免Block中的循环引用。这是由于当Block从栈复制到堆时,若Block使用的变量为附有__block说明符的id类型或对象类型的自动变量,不会被retain;若Block使用的变量为没有__block说明符的id类型或对象类型的自动变量,则被retain。

    注意:正好在ARC有效时能够同__unsafe_unretained修饰符一样来使用。由于ARC有效时和无效时__block说明符的用途有很大的区别,因此在编写源代码时,必须知道该源代码是在ARC有效情况下编译还是在ARC无效情况下编译。

    相关文章

      网友评论

          本文标题:iOS开发读书笔记:Objective-C高级编程 iOS与OS

          本文链接:https://www.haomeiwen.com/subject/qmdlwftx.html