iOS Objective-C 基础 (二) - 类的属性 / Block

iOS Mar 9, 2015

iOS 的各种接口见识完之后,又想回炉了,想多了解下底层的一些东西,刚好最近在学 Swift,他俩比较像,索性一锅端了。

@property

@property (nonatomic, copy) NSString *recievedContent;

以前定义字符串这么写,只记得用 nonatomic 和 copy 就能定义字符串,虽说 copy 的作用略知一二,但为啥这么写还是不清楚。还是基础不够扎实。

这次要说的内容是 Objective-C 中的 @property和 @synthesize。在这之前先讲讲访问器(Accessor),也就是我们所知道的 setter 和 getter 方法。访问器是很重要的技术,用来访问和设置对象的实例变量(不是指对象本身,而是对象中的属性)。有时候可能需要用不同的方式或者通过计算等方式来获取或设置实例变量,访问器给了我们很大的灵活性。在 Cocoa 中访问器有很多的优点:

  • 实现灵活性。 可以在访问器中改变并实现不同的实例变量访问方式而不影响其他代码。
  • 可维护性。通过访问器对实例变量的更改易于维护。
  • 支持 KVC 和 KVO。 KVC 和 KVO 是很强大的技术。但是它们依赖于正确命名访问器。

下面这段代码简单的实现了一个访问器(setter 和 getter):

{
	NSString *_recievedContent;
}

//setter
-(void)setRecievedContent:(NSString *)recievedContent {
     _recievedContent = recievedContent;
}

//getter
-(NSString *)recievedContent {
     return recievedContent;
}

这么写的确很麻烦,有没有更好的一种方法呢?当然有啊,用 @property!

@property (nonatomic, copy) NSString *recievedContent;

这么写跟上面那几行等效的,那么问题又来了,nanotomic 和 copy 是做啥的。

这些关键字都是有用的,大致分为三类:

  • 原子性
  1. atomic(默认):atomic 意为操作是原子的,意味着只有一个线程访问实例变量。atomic 是线程安全的至少在当前的访器上我是安全的。它是一个默认的,但是很少使用。它的比较慢,这跟 ARM 平台和内部锁机制有关。

2. nonatomic:跟 atomic 刚好相反。表示非原子的,可以被多个线程访问。它的速度比atomic快。但不能保证在多线程环境下的安全性,在单线程和明确只有一个线程访问的情况下广泛使用。

  • 访问控制
  1. readwrite(默认):readwrite 是默认的,表示同时拥有 setter 和 getter。
  2. readonly:readonly 表示只有 getter 没有 setter。
  • 内存管理
  1. retain:使用了 retain 意味着实例变量要获取传入参数的所有权。具体表现在 setter 中对实例变量先 release 然后将参数 retain 之后传给它。
  2. assign(默认):用于值类型,如 int, float, double 和 NSInteger, CGFloat 等表示单纯的复制。还包括不存在所有权关系的对象,比如常见的 delegate。
  3. strong:是在 ARC 伴随 iOS 引入的时候引入的关键字是 retain 的一个可选的替代。表示实例变量对传入的参数要有所有权关系即强引用。strong 跟 retain 的意思相同并产生相同的代码,但是语意上更好更能体现对象的关系。
  4. weak: weak 跟 assign 的效果相似,不同的是 weak 在对象被回收之后自动设置为 nil。而且 weak 只能用在 iOS5 或以后的版本,对于之前的版本,使用 unsafe_unretained。
  5. unsafe_unretained:weak 的低版本替代。
  6. copy:copy 是为是实例变量保留一个自己的副本。

@synthesize

现在知道 @property 的用法了,再回过头说 @synthesize。

Xcode4.4 之前,还需要写这么一句:

@synthesize recievedContent = _recievedContent;

@synthesize 作用就是生成 setter 和 getter 方法。不过在之后的版本中,如果什么都不写,默认就是 @syntheszie var = _var;,目的就是生成 setter 和 getter 方法。

当然了,也可以手动写成 @syntheszie recievedContent,或者 @syntheszie recievedContent = anythingElse,就可以通过指针的方式用 recievedContent,或者 anythingElse 直接调用变量,而不是 self.recievedContent

和这个类似的,还有一个 @dynamic 关键字,代表用户要自己实现 setter 和 getter 方法。如果你不写,编译的时候倒是能通过,运行的时候,就会 Crash。

iOS 声明变量的三种方法

  • 方式一:@interface 内直接大括号定义
  • 方式二:大括号中定义完之后再用 @property,最后在 .m 文件中加入 @synthesize
  • 方式三:直接用 @property 声明
@interface SelfInfoTableViewController () {

    NSArray *_basicInfo;
}
@end
@interface SelfInfoTableViewController () {

    NSArray *_basicInfo;
}

@property (nonatomic, strong) NSArray *basicInfo;
@end

@synthesize basicInfo = _basicInfo
@interface SelfInfoTableViewController ()

@property (nonatomic, strong) NSArray *basicInfo;
@end

方式一和方式三的区别是,方式一定义的变量只能在自己内部使用,而不能在类外部使用,方式三则相反,内部使用的话,需要 _basicInfo,或者 self.basicInfo

Xcode4 之后,基本摒弃了 @synthesize,Xcode 会自动生成 setter 和 getter,所以方式二不太常见。

Objective-C 的 Block 非常神奇,相当于一个类,里面保存的是一段代码(代码块),别的语言可能还没有这个特性,同时,Apple 官方也推荐开发者使用 Block 来写程序。

Objc 中的 Block

Block 跟函数挺像的,不过用法比函数要高大上。举个栗子:

// 函数是这样写
void blockTest() {
    NSLog(@"Hello, World!");
}

// 这样调用
blockTest();

// block 这样写
void (^blockTest)() = ^{
    NSLog(@"Hello, World!");
};

// 这样调用
blockTest();

两个调用是完全一样的,不过 Block 的声明有点难理解,来分析下

void 说明函数没有返回值,跟函数的写法完全一样的,

(^blockTest) 拆成三部分,blockTest 是 Block 的名字,前面加一个 ^ 说明这是个 Block 类型,然后再用括号括住,这是固定写法,

() 说明无参数,

^{} 这个就是 Block 内容的固定写法了,当然前提是没有参数,另一种情况稍后补充慢慢来~

有参数无返回值的 Block

void (^blockTest)(int, NSString *) = ^(int count, NSString *word){
    
    for (int i = 0; i<count; i++) {
        NSLog(@"%@", word);
    }
};

blockTest(5, @"Hello, World!");

大括号内的东西大概都明白,不再细说,上面提到,() 代表无参数,那么这次里面有一个 int, NSString *,说明有两个类型的参数,后面原本是 ^{},现在中间多了个 (),道理同上。

有参数有返回值的 Block

int (^sum)(int, int) = ^(int x, int y){
    
    return x + y;
};

NSLog(@"%d", sum(13, 14));

这个其实我不想多说了,以大家聪明的程度肯定自己能看懂。

Block 中的 typedef

大家都知道 typedef 可以这么用

typedef int BlockInt;

相同的,可以推理到 Block 中

typedef int (^MyBlock)(int);

注意写法,跟普通的不太一样

之后想用的时候,可以直接把 MyBlock 当成一个类型,举个栗子

MyBlock square = ^(int x){
    return x * x;
};

square(6);

Block 到底(比函数)优越在哪里

void getSomeInfo(NSString *info) {
    
    NSLog(@"%@", info);
}

用函数写的话,可以这么玩,传入一个已知类型的变量,由这个函数来决定如何处置变量。

我们写程序讲究低耦合,这个函数关心的东西越少越好,这么做管的太宽了,但是我们有 Block,来看一个神奇的效果:

void getSomeInfo(void(^blockFunc)()) {
    
    NSLog(@"Before Block");
    blockFunc();
    NSLog(@"After Block");
}

main 函数中

getSomeInfo(^{
    
    NSLog(@"%@", @"You can do anything here");
});

结果是

2015-05-21 22:52:23.498 Block-Test[5427:277676] Before Block
2015-05-21 22:52:23.499 Block-Test[5427:277676] You can do anything here
2015-05-21 22:52:23.499 Block-Test[5427:277676] After Block

当然这么搞不太严谨的,如果传进去一个空,程序会报错(坏访问),解决方法也很简单,判断一下是否为空即可,我就不写了 =. =

Block 的进阶使用

修改外部变量的时候,外部变量需要加一个 __block 来修饰

__block int a = 50;

为什么加上 __block 就能修改了

先贴 main.m 的代码

//
//  main.m
//  Block-Test
//
//  Created by JieLee on 15/5/21.
//  Copyright (c) 2015年 PUPBOSS. All rights reserved.
//

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        __block int a = 10;
        
        void (^blockTest)() = ^{
            
            a = 50;
            NSLog(@"%d", a);
        };

        blockTest();
        
    }
    return 0;
}

在 Terminal 中,输入 clang -rewrite-objc main.m,居然转换了 10 万多行 C++ 代码,拉到最下面分析

有一段是这样的

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

        void (*blockTest)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);

        ((void (*)(__block_impl *))((__block_impl *)blockTest)->FuncPtr)((__block_impl *)blockTest);

    }
    return 0;
}

来分析下,一般括号内的东西是强转,那就好办了,重点看这句

void (*blockTest)() = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344);

删掉各种括号,就变成了

void (*blockTest)() = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344);

__main_block_impl_0 应该是一个类,或者结构体,我们往上找,还真有一个

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

C++ 的结构体比较有意思,他可以调用自己的 __main_block_impl_0 函数,然后把需要的东西传进去,最后 Desc = desc; 返回一个结构体。

再转回来 &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &a, 570425344); 是什么意思呢,返回一个结构体,然后取地址,所以 block 的本质,就是一个结构体!

至于为什么能改变 a 的值,我贴段代码,大家自己理解吧,我是有点蒙。反正就是各种地址各种传进去,然后就把值给改了。

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref


            (a->__forwarding->a) = 50;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_f__kvyhp79j5wbcb2d7t24v3v_c0000gn_T_main_f979f0_mi_2, (a->__forwarding->a));
        }

文章最后有完整的代码,想研究的可以去看看~

附完整代码

https://gist.github.com/pupboss/0cf42d6114816ef2fdb8

Tags

Jie Li

🚘 On-road / 📉 US Stock / 💻 Full Stack Engineer / ®️ ENTJ