iOS检测私有Api的方法

  1. 首先你有个可以提交审核的ipa,可以先将项目 archive, 在window->organizer中,选择distribute app -> ios app store -> export,将ipa导出到本地中
  2. 将ipa解压,会有两个文件夹Payload、Symbols
  3. cd到Payload里面的app  
  4. 有两种方式可以检测打包文件是否包含字符串          
    • strings - -a -arch armv7 “AAAA” | grep 你想搜的方法名           
    • strings - -a -arch armv7 “AAAA” > test.txt

   

第二种可以自己去生成的test.text文件中搜索相应方法名字

谈block、__weak和__strong

最近在”翻新”公司的老项目的时候,发现一个奇怪的问题:

在一个 block 中,我使用了 RAC 为了避免 block 循环引用而定义的两个宏: @weakify@strongify,但是如果在 block 内部使用下划线属性(成员变量),还是会导致循环引用。

很多人都知道怎么处理这个问题,在使用了@weakify@strongify的情况下,在 block 内部像self -> ivar这样使用成员变量就可以避免循环引用了,但是为什么这样用就没问题呢?使用了@weakify@strongify两个宏之后发生了什么呢?带着你在使用 block 时出现过的疑问,在后面的内容中你可能会得到答案。

block是什么

block 是用于创建匿名函数的 C 语言扩展。用户使用 block 指针与 block 对象进行交互并传输 block 对象,block 指针表示为普通指针。block 可以从局部变量中捕获值;发生这种情况时,必须动态分配内存。初始分配在栈上完成,但 runtime 提供了一个Block_copy函数,给定一个 block 指针,将底层 block 对象复制到堆中,将其引用计数设置为1并返回新的 block 指针,或者(如果 block 对象已经在堆上)将其引用计数增加1.配对函数是Block_release,它将引用计数减少1并在计数达到零并且在堆上时销毁对象。翻译自苹果文档

上面的翻译来自于 谷歌翻译~。我对于 block 的理解就是一个指针,指向一个带有函数指针 (用于执行block内的代码) 的结构体,该结构体内有许多捕获的成员变量。在 ARC 环境下 block 会从 栈中自动复制到堆中,方便 runtime 管理内存生命周期;如果内部有全局变量则复制到数据区,生命周期为程序创建到程序结束。

[站外图片上传中…(image-1d1a52-1561035272667)]

block的数据结构

block 的数据结构定义如下
[站外图片上传中…(image-f5b34b-1561035272667)]

结构体定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;// sizeof(struct Block_layout)
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

通过它的数据结构,我们知道一个 block 实际上是由5部分组成的

  1. isa 指针,所有对象都有该指针,用于实现对象相关的功能
  2. flags,用于按 bit 位表示一些 block 的附加信息
  3. reserved,保留变量
  4. invoke,函数指针,指向具体的 block 实现的函数调用地址
  5. descriptor, 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针

block的几种的类型

常见的 block 有下面三种,不用类型的 block 存放不同的区域,在 ARC 环境下只有_NSConcreteGlobalBlock_NSConcreteMallocBlock两种类型的 block

  • _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  • _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  • _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

下面是详细的介绍

_NSConcreteStackBlock

该类型的 block 仅存在在 MRC 环境中,数据存放在栈区,当函数返回时会被销毁。在 ARC 环境中,不存在_NSConcreteStackBlock类型,只存在_NSConcreteGlobalBlock_NSConcreteMallocBlock 两个类型。在下面的例子中, block 的类型的打印结果是__NSMallocBlock__。原因可能是因为c语言的结构体中,编译器不能很好地管理初始化和销毁,这样对内存管理来说很不方便,所以就将 block 放到堆上,使用 runtime 来管理它们的生命周期。

1
2
3
4
5
6
7
8
9
10
int val = 1;

void(^textBlock)(void) = ^{
NSLog(@"[block] val<%p>: %d", &val, val);
NSLog(@"val: %d", val);
};
NSLog(@"val<%p>: %d", &val, val);
textBlock();
NSLog(@"textBlock: %@", textBlock);

打印结果为:

1
2
3
4
val<0x16b523d1c>: 1
[block] val<0x280a9fcb0>: 1
textBlock: <__NSMallocBlock__: 0x28076a4c0>

下面使用 clang -rewrite-objc filename 将代码转换成 C++ 的实现, 下面是关键部分的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
int val;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy

}

static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0)};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {
static int static_v = 1;
int val = 1;

void(*textBlock)(void) = ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, val));
((void (*)(__block_impl *))((__block_impl *)textBlock)->FuncPtr)((__block_impl *)textBlock);

}

  • 其中__MyObject__test_block_impl_0 是 block 的结构体类型
  • __MyObject__test_block_func_0 是 block 实现的函数,在 __MyObject__test_block_impl_0内有一个指针FuncPtr 指向该函数
  • __MyObject__test_block_desc_0 是 block 附件描述信息的结构体,包含着 block 结构体大小, copy 和 dispose 函数指针(这两个函数后面后讲到)等的描述信息,在 __MyObject__test_block_impl_0内有一个指针Desc 指向该结构体
  • _I_MyObject_test 函数内可以看到 block 的初始化,void(*textBlock)(void) 说明 textBlock 是一个指向该 block 结构体的指针

首先观察这个__MyObject__test_block_impl_0的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
int val;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

  • 使用 clang 转换过的实现是 MRC 环境的,所以 isa 指针指向 _NSConcreteStackBlock 类型
  • 在这个结构体中可以看到一个成员变量int val; ,没错,它就是 block 捕获的局部变量,从构造函数 __MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int _val, int flags=0) : val(_val) 中可以看到,block 仅仅捕获了该变量的值
  • __MyObject__test_block_impl_0中由于增加了一个变量 val,所以结构体的大小变大了,结构体大小被写在了__MyObject__test_block_desc_0
  • block 捕获外部变量仅仅只 block 闭包里面会用到的值,其他用不到的值,它并不会去捕获。

再看一下__MyObject__test_block_func_0这个函数的实现:

1
2
3
4
5
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy

}

我们可以发现,系统自动给我们加上的注释,bound by copy,自动变量 val 虽然被捕获进来了,但是是用 __cself->val 来访问的。block 仅仅捕获了 val 的值,并没有捕获 val 的内存地址。所以在__MyObject__test_block_func_0 这个函数中即使我们重写这个自动变量 val 的值,依旧没法去改变block外面变量 val 的值。

小结一下:
基本数据类型的变量是以值传递方式传递到 block 的构造函数里面去的。block 只捕获 block 中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以 block 内部不能改变变量的值。

_NSConcreteMallocBlock

修改一下上面的代码:

1
2
3
4
5
6
7
8
9
10
 __block int val = 1;
void(^textBlock)(void) = ^{
val++;
NSLog(@"[block] val<%p>: %d", &val, val);
};
NSLog(@"val<%p>: %d", &val, val);
textBlock();
NSLog(@"val<%p>: %d", &val, val);
NSLog(@"textBlock: %@", textBlock);

打印输出为:

1
2
3
4
5
val<0x282db0858>: 1
[block] val<0x282db0858>: 2
val<0x282db0858>: 2
textBlock: <__NSMallocBlock__: 0x2823d3450>

重新用 clang 生成的c++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};

struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref

(val->__forwarding->val)++;
}
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};
void(*textBlock)(void) = ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
((void (*)(__block_impl *))((__block_impl *)textBlock)->FuncPtr)((__block_impl *)textBlock);
}

在重新生成的代码中,我们看到新增了一个名为__Block_byref_val_0的结构体,它是用来替代我们__block修饰的变量 val 的。

  • 它的第一个指针是 isa,说明它也是一个对象。
  • 第二个指针是指向自身类的指针__forwarding
  • 第三个是一个标记 flag
  • 第四个是结构体的大小
  • 第五个是变量 val 的值

在函数static void _I_MyObject_test(MyObject * self, SEL _cmd)我们可以看到该结构体的初始化代码:

1
2
3
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 1};


  • 在初始化时, isa 指向了一个空指针
  • __forwarding指向了自己的地址
  • 1是变量 val 的值。

使用 __block修饰的变量,无论是基本数据类型还是 OC 的类,在编译之后都是转换成一个新的结构体,该结构体的__forwarding指针会指向自己的地址,而成员变量 val 则为编译前的类型和值。至于这样的目的是什么,可以接着看下面。


1
2
3
4
5
6
7
static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

__MyObject__test_block_desc_0 这个结构体中,我们发现比之前的代码多了一个 copydispose 的函数指针。在c语言的结构体中,编译器没有很好地进行初始化和销毁,这样对内存管理来说很不方便,所以就在增加了这两个函数指针,方便进行内存管理。copy函数把block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

  • copydispose这两个函数指针对应的两个函数实现
1
2
3
4
5
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}


  • __MyObject__test_block_copy_0函数实现中出现了方法_Block_object_assign,
  • __MyObject__test_block_dispose_0函数实现中出现了方法_Block_object_dispose

下面是这两个方法的申明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

// Create a heap based copy of a Block or simply add a reference to an existing one.
// This must be paired with Block_release to recover memory, even when running
// under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

// Used by the compiler. Do not call this function yourself.
BLOCK_EXPORT void _Block_object_dispose(const void *, const int)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

下面是这两个方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;

// 1
if (!arg) return NULL;

// 2
aBlock = (struct Block_layout *)arg;

// 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}

// 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}

// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;

// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

// 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;

// 8
result->isa = _NSConcreteMallocBlock;

// 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}

return result;
}

void _Block_release(void *arg) {
// 1
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;

// 2
int32_t newCount;
newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;

// 3
if (newCount > 0) return;

// 4
if (aBlock->flags & BLOCK_NEEDS_FREE) {
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
_Block_deallocator(aBlock);
}

// 5
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
;
}

// 6
else {
printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
}
}

  • _Block_copy_internalBlock_copy的一个实现,实现了从_NSConcreteStackBlock复制到_NSConcreteMallocBlock的过程,有9个步骤。

    • 在第8步中我们可以看到 isa 指针指向了_NSConcreteMallocBlock
  • _Block_releaseBlock_release的一个实现,实现了一个block释放的过程,有6个步骤


扯的有点远了,现在让我们总结一下 __block 修饰的变量在block内发生了什么。

  • block 会在栈中被创建,然后通过Block_copy函数复制到堆中。由 runtime 管理它的生命周期
  • 使用 __block 修饰的变量,在编译后会变成一个新的对象。在初始化时,成员变量__forwarding 会指向栈中该变量的地址,val 为该变量原本的值。当 block 的成员变量 __Block_byref_val_0 从栈中复制到堆中时,成员变量 __Block_byref_val_0的地址可能改变了,但是 __forwarding 指针指向的结构体是不会变的,仍然在栈中。
  • block 的实现函数__MyObject__test_block_func_0,block 通过 __Block_byref_val_0 *val = __cself->val;(val->__forwarding->val)++ 变量的地址修改 val,所以在 block 内部修改变量 val 是会影响到 block 外部的变量。
  • 这就是为什么 block 内部和外部 val 的地址不同的原因(一个在栈上,一个在堆上)。因为他们__forwarding指向的结构体是一样的,所以在 block 内部修改变量会影响到外部,

_NSConcreteGlobalBlock

block 内部只用到全局变量,包括全局变量静态全局变量静态变量,以及上述 block 的 copy 版本。数据存放在数据区,生命周期从应用创建到应用结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int global_v = 1;
static int static_global_v = 1;

@implementation MyObject

- (void)test
{
static int static_v = 1;

NSLog(@"val<%p>: %d", &static_v, static_v);
NSLog(@"global_v<%p>: %d", &global_v, global_v);
NSLog(@"static_global_v<%p>: %d", &static_global_v, static_global_v);

void(^textBlock)(void) = ^{
static_v++;
global_v++;
static_global_v++;
NSLog(@"[block] val<%p>: %d", &static_v, static_v);
NSLog(@"[block] global_v<%p>: %d", &global_v, global_v);
NSLog(@"[block] static_global_v<%p>: %d", &static_global_v, static_global_v);
};
textBlock();
NSLog(@"textBlock: %@", textBlock);
}

打印信息为:

1
2
3
4
5
6
7
8
9
10
val<0x1034b8114>: 1
global_v<0x1034b8110>: 1
static_global_v<0x1034b8118>: 1

[block] val<0x1034b8114>: 2
[block] global_v<0x1034b8110>: 2
[block] static_global_v<0x1034b8118>: 2

textBlock: <__NSGlobalBlock__: 0x10343da40>

clang 之后 C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int global_v = 1;
static int static_global_v = 1;


struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
int *static_v;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, int *_static_v, int flags=0) : static_v(_static_v) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
int *static_v = __cself->static_v; // bound by copy

(*static_v)++;
global_v++;
static_global_v++;
}

static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0)};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {
static int static_v = 1;
}

  • block 仅仅捕获了静态变量 static_v 的地址作为自己的成员变量,因此在内部修改该变量可以影响到 block 外部。block 内部和外部该变量的地址相等
  • 全局变量 global_v 和全局静态变量 static_global_v 并没有被 block 捕获,因为他们已经被保存在数据区中,可以直接使用

由于 clang 改写的方式跟 LLVM 不太一样,在这里并没有开启ARC,所以这里我们看到 isa 指向的还是 _NSConcreteStackBlock,但在开启ARC的时候,block 应该是 _NSConcreteGlobalBlock 类型。

block 与 self

在前面的部分,我们已经分析过 局部变量,静态变量,全局变量,全局静态变量在 block 时的情况,那么,还有一种特殊的变量 self,它在 block 内部时又是怎么样运行的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@interface MyObject () {
NSString *_age;
}
@property (nonatomic, strong) NSString *name;
@end

@implementation MyObject

- (void)test
{

self.name = @"n";
_age = @"10";
NSLog(@"self: %@", self);

void(^textBlock)(void) = ^{
self.name = @"a";
_age = @"11";

NSLog(@"[block] self: %@", self);
NSLog(@"[block] name<%p>: %@", self.name, self.name);
NSLog(@"[block] age<%p>: %@", _age, _age);
};
NSLog(@"name<%p>: %@", self.name, self.name);
NSLog(@"age<%p>: %@", _age, _age);
textBlock();
NSLog(@"name<%p>: %@", self.name, self.name);
NSLog(@"age<%p>: %@", _age, _age);
}

打印结果:

1
2
3
4
5
6
7
name<0x102804818>: n
age<0x102804838>: 10
[block] name<0x102804858>: a
[block] age<0x102804878>: 11
name<0x102804858>: a
age<0x102804878>: 11

clang之后的C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
MyObject *self;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
MyObject *self = __cself->self; // bound by copy

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_2);
(*(NSString **)((char *)self + OBJC_IVAR_$_MyObject$_age)) = (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_3;

}
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_0);
(*(NSString **)((char *)self + OBJC_IVAR_$_MyObject$_age)) = (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_1;

void(*textBlock)(void) = ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, self, 570425344));
((void (*)(__block_impl *))((__block_impl *)textBlock)->FuncPtr)((__block_impl *)textBlock);
}

  • __MyObject__test_block_impl_0中我们可以看到self也被 block 捕获成了成员变量
  • __MyObject__test_block_impl_0的构造函数中我们可以看到 self 被当做参数被传入,而不是 self 的地址
  • 因为 block 在内部和外部 self 指向的是相同的 MyObject 结构体,所以在 block 内部对 self 成员变量进行修改会影响到 block 外部
  • block 的结构体会强引用 self,所以需要小心使用,否则会引起循环应用
1
2
3
4
5
6
7
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
MyObject *self = __cself->self; // bound by copy

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)self, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_2);
(*(NSString **)((char *)self + OBJC_IVAR_$_MyObject$_age)) = (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_2519b6_mi_3;
}

block 内部使用属性和成员变量是不一样的。直接使用属性时,走的是 obj_msgSend 消息发送(具体可以研究这篇博客),而在使用成员变量时,应该是先通过 self 得到结构体的首地址,然后通过成员变量的偏移量然直接使用这个成员变量(其实我也没很理解。。。)

小结一下:

  • block 内部使用 self 时的情况跟使用局部变量的情况是比较类似的,block 会捕获 self 的值而不是地址当做成员变量
  • 在 block 内部使用属性和成员变量的情况是不一样的

__weak与__strong

我们都知道使用__weak和__strong修饰符可以避免在block的使用中出现循环引用的问题,这是为什么呢?先让我们了解一下这两个修饰符吧!

ARC 环境下,OC的对象面前都需要加上所有权的修饰符,所有的修饰符有以下4种

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

默认的修饰符是__strong。

ARC下,self既不是strong也不是weak,而是unsafe_unretained的,也就是说,入参的self被表示为:(init系列方法的self除外)来源:博客

1
2
3
4
- (void)start {
const __unsafe_unretained MyObject *self;
}

想要弄清__weak与__strong的实现原理,需要研究一下clang中关于ARC的文档,有兴趣可以点进去仔细看看。

__strong

1
2
id __strong object = [[NSObject alloc] init];

在终端使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m转换成 C++ 的实现

1
2
id __attribute__((objc_ownership(strong))) object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

= 右边的代码意思应该是对 NSObject 这个类发送 alloc 消息,然后再对生成的对象发送 init 消息,这两个方法的实现可以在 runtime 中找到,代码我也贴到下面了
= 左边的代码,我不大理解objc_ownership这个函数,查了下搜不到是啥意思,看字面意思应该是两个对象间的持有关系,也就是自己持有自己的意思。

1
2
3
4
5
6
7
8
9
+ alloc
{
return (*_zoneAlloc)((Class)self, 0, malloc_default_zone());
}
- init
{
return self;
}

__weak

1
2
3
id __strong object = [[NSObject alloc] init];
id __weak weakSelf = object;

在终端使用命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m转换成 C++ 实现

1
2
3
id __attribute__((objc_ownership(strong))) object = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
id __attribute__((objc_ownership(weak))) weakSelf = object;

相应的会调用

1
2
3
objc_initWeak(&weakSelf,object);
objc_destoryWeak(&weakSelf);

objc_initWeak方法的文档说明
Precondition: object is a valid pointer which has not been registered as a __weak object. value is null or a pointer to a valid object.
If value is a null pointer or the object to which it points has begun deallocation, object is zero-initialized. Otherwise, object is registered as a __weak object pointing to value. Equivalent to the following code:

id objc_initWeak(id *object, id value) {
*object = nil;
return objc_storeWeak(object, value);
}

这个函数会把传入的 object 置为nil,然后执行objc_storeWeak函数。


那么objc_storeWeak函数是干什么的呢?下面是这个方法的说明

Precondition: object is a valid pointer which either contains a null pointer or has been registered as a __weak object. value is null or a pointer to a valid object.
If value is a null pointer or the object to which it points has begun deallocation, object is assigned null and unregistered as a __weak object. Otherwise, object is registered as a __weak object or has its registration updated to point to value.
Returns the value of object after the call.

objc_storeWeak函数的用途就很明显了。由于weak表也是用Hash table实现的,所以objc_storeWeak函数就把第一个入参的变量地址注册到weak表中,然后根据第二个入参来决定是否移除。如果第二个参数为0,那么就把__weak变量从weak表中删除记录,并从引用计数表中删除对应的键值记录

所以如果__weak引用的原对象如果被释放了,那么对应的__weak对象就会被指为nil。原来就是通过objc_storeWeak函数这些函数来实现的。


接下来是 objc_destoryWeak 函数的实现

1
2
3
4
void objc_destroyWeak(id *object) { 
objc_storeWeak(object, nil);
}

还是调用上面的objc_storeWeak函数,因为传入的value为nil,所以object将从weak表中删除并且置为nil

__weak与__strong的作用

终于讲到这两个所有权修饰符的作用了。


首先是不使用这两个修饰符时的情况。在上面我们已经讲到过 block 存在 self 的一种情况了,下面我们要讲一下 block 存在 self 并且 self 强应用 block 时的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@interface MyObject ()
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void(^textBlock)(void);
@end

@implementation MyObject
- (void)test
{
self.textBlock = ^{
self.name = @"n";
}
}

@end

@implementation OneViewController

- (void)viewDidLoad {
[super viewDidLoad];

self.object = [[MyObject alloc] init];
[self.object test];
}


对于 MyObject 来说是造成了循环引用的,因为它强引用了 block,而 block 内部也强引用着 self,所以 MyObject 是不能被dealloc的,但奇怪的是,将 MyObject 当做属性的 OneViewController 竟然可以dealloc,这估计是另一个问题了,等我有空再去研究一下这个。。。

使用 clang 得到的C++实现,这边只截取了block结构体和初始化block部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
MyObject *self;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *_self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 初始化
((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, self, 570425344)

在这个部分中可以看到 block 将 self(MyObject *指针)捕获成了自己的成员变量了(强引用), 而self指针的成员变量又包含block,造成循环引用。


仅仅使用__weak

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void(^textBlock)(void);
@end

@implementation MyObject

- (void)test
{
__weak typeof(self) weakSelf = self;
self.textBlock = ^{
weakSelf.name = @"n";
NSLog(@"hh");
};
self.textBlock();
}

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m得到C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
MyObject *const __weak weakSelf;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
MyObject *const __weak weakSelf = __cself->weakSelf; // bound by copy

((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)weakSelf, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_970d18_mi_0);
}
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {

__attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setTextBlock:"), ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, weakSelf, 570425344)));
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("textBlock"))();
}

苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。当被指向的对象执行 dealloc 时候,将所有指向该对象的 weak 指针的设置为nil。

  • block 将 __weak 修饰的 self 捕获为成员变量
  • 当 self 执行dealloc时,block 内的 self 置为nil,从而打破循环引用
  • 当 self delloac 之后,在调用 block 的函数指针,block 内部的self置为nil。

同时使用__weak与__strong

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface MyObject ()
//{
// NSString *_age;
//}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, copy) void(^textBlock)(void);
@end

@implementation MyObject

- (void)test
{
__weak typeof(self) weakSelf = self;
self.textBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf.name = @"n";
NSLog(@"hh");
};
self.textBlock();
}

使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 MyObject.m得到C++实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
struct __MyObject__test_block_impl_0 {
struct __block_impl impl;
struct __MyObject__test_block_desc_0* Desc;
MyObject *const __weak weakSelf;
__MyObject__test_block_impl_0(void *fp, struct __MyObject__test_block_desc_0 *desc, MyObject *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __MyObject__test_block_func_0(struct __MyObject__test_block_impl_0 *__cself) {
MyObject *const __weak weakSelf = __cself->weakSelf; // bound by copy

__attribute__((objc_ownership(strong))) typeof(weakSelf) strongSelf = weakSelf;
((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)strongSelf, sel_registerName("setName:"), (NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_0010b9_mi_0);
}
static void __MyObject__test_block_copy_0(struct __MyObject__test_block_impl_0*dst, struct __MyObject__test_block_impl_0*src) {_Block_object_assign((void*)&dst->weakSelf, (void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__test_block_dispose_0(struct __MyObject__test_block_impl_0*src) {_Block_object_dispose((void*)src->weakSelf, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__test_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __MyObject__test_block_impl_0*, struct __MyObject__test_block_impl_0*);
void (*dispose)(struct __MyObject__test_block_impl_0*);
} __MyObject__test_block_desc_0_DATA = { 0, sizeof(struct __MyObject__test_block_impl_0), __MyObject__test_block_copy_0, __MyObject__test_block_dispose_0};

static void _I_MyObject_test(MyObject * self, SEL _cmd) {

__attribute__((objc_ownership(weak))) typeof(self) weakSelf = self;
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)self, sel_registerName("setTextBlock:"), ((void (*)())&__MyObject__test_block_impl_0((void *)__MyObject__test_block_func_0, &__MyObject__test_block_desc_0_DATA, weakSelf, 570425344)));
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)self, sel_registerName("textBlock"))();
}

  • __MyObject__test_block_impl_0 block 仍然是将 __weak 修饰的 self 捕获为成员变量
  • 当 self 执行dealloc时,block 内的 self 会被置为nil,从而打破循环引用
  • block 内的代码在__MyObject__test_block_func_0函数内,当使用strongSelf时,会先取出__weak修饰的成员变量self:MyObject *const __weak weakSelf = __cself->weakSelf;, 然后再生成一个__strong修饰的局部变量__attribute__((objc_ownership(strong))) typeof(weakSelf) strongSelf = weakSelf;,self 的引用计数 +1。这样的目的是在 block 内的代码块执行完之前避免 self 被dealloc掉。当 block 执行完毕之后,局部变量 strongSelf 被释放,self 的引用计数 -1。

@weakify 和 @strongify

这两个是RAC中避免Block循环引用而开发的2个宏,实现过程很牛,值得我们学习。限于篇幅,我就不分析了,想了解可以点开这篇博客
这两个宏展开下来就相当于:

1
2
3
4
@weakify(self) = @autoreleasepool{} __weak __typeof__ (self) self_weak_ = self;
@strongify(self) = @autoreleasepool{} __strong __typeof__(self) self = self_weak_;


回到开头

好了,不知道你看了这么多头晕了没有。。。下面让我们回到开头我碰到的那个问题,为什么我使用了 @weakify 和 @strongify,然后直接使用下划线的成员变量还是会造成循环引用。原因就是_ivar直接使用成员变量,self 跟 weakSelf会同时被 block 捕获成 block 的成员变量,注意:self 还是会被 block 捕获的(前面好像没写例子,不过你可以自己写写看),导致 block 还是强引用着 self,导致循环引用。解决办法就是 strongSelf -> ivar这样使用成员变量

总结

  • block 会捕捉 block 内部的变量

    • 当变量类型是局部变量(基本数据类型时或 oc 类),仅捕获该变量的值,所以 block 内部和外部这两个变量的地址是不一样的,在block 内部修改变量的值也不会影响 block 外部的变量
    • 当变量是 self 时的情况跟 局部变量时是差不多的
    • 当变量类型是__block修饰的布局变量(基本数据类型或者 oc 类),会新构建一个结构体,其中成员变量__forwarding会指向栈中该变量的地址,因此在 block 内部修改该变量会影响 block 外部的变量
    • 当变量是全局变量或者全局静态变量时,block 不会捕获该变量,因为变量已经存在在数据区,可以直接调用。此时 block 也保存在数据区
    • 当变量是静态变量时,block 会捕获该变量的地址,因此在 block 内部修改该变量会影响 block 外部的变量
  • block 结构体中的成员变量 descriptor 包含着 copydispose 两个函数指针。copy 函数把 block 从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

  • 苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。当被指向的对象执行 dealloc 时候,将所有指向该对象的 weak 指针的设置为nil。

  • 在 block 外部使用 __weak 的原因是,让 block 将这个 __weak修饰的变量捕获成自己的成员变量,这样当外面的变量被 dealloc 后,block 内的该成员变量也将置为 nil,避免循环引用

  • 在 block 里面使用的 __strong 修饰的 weakSelf 是为了在函数生命周期中防止 self 提前释放。strongSelf是一个局部变量,当block内的代码执行完毕就会释放,不会对 self 进行一直进行强引用。

引用

ARC对self的内存管理
深入研究 Block 捕获外部变量和 __block 实现原理
深入研究 Block 用 weakSelf、strongSelf、@weakify、@strongify 解决循环引用
谈Objective-C block的实现

[self class]和[super class]

今天在学习runtime的时候,碰到一个有意思的题目,相信很多人都曾经看到过:

1
2
3
4
5
6
7
8
9
10
11
@implementation Son : Father

- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

输出为Son和Son,为什么呢?当我们使用 clang -rewrite-objc Son.m文件,可以看到

1
2
NSLog((NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_bf216e_mi_1, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_xy_nncr8hn96cd0_rdymw2f0qcw0000gn_T_MyObject_bf216e_mi_2, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Son"))}, sel_registerName("class"))));

这里需要明白以下几个概念:

方法中的隐藏参数 self

我们经常在方法中使用self关键字来引用实例本身,但从没有想过为什么self就能取到调用当前方法的对象吧。其实self的内容是在方法运行时被偷偷的动态传入的。当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:

  • 接收消息的对象(也就是self指向的内容)
  • 方法选择器(_cmd指向的内容)
    之所以说它们是隐藏的是因为在源代码方法的定义中并没有声明这两个参数。它们是在代码被编译时被插入实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。
1
2
static void _I_MyObject_test(MyObject * self, SEL _cmd) {
}

使用 clang 将代码转换成C++实现,我们可以看到方法的两个隐藏参数 self 和 _cmd

objc_msgSend 和 objc_msgSendSuper的定义

先给出 objc_msgSend 和 objc_msgSendSuper的定义:

1
2
3
id objc_msgSend(id self, SEL op, ...)
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

而 ‘objc_super’的定义如下:

1
2
3
4
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};

- (Class)class 方法的实现

1
2
3
- (Class)class {
return object_getClass(self);
}

现在再让我们看一下调用 [super class] 会发生什么

  • 当我们对 super 关键字发送消息时,编译器会创建一个 objc_super 的结构体,其中 receiver 仍旧为 self,而super_class为父类
  • 调用 objc_msgSendSuper()方法,会去 Father 的实例方法列表中寻找 class 这个方法,找不到,去 NSObject 中查找
  • 调用 NSObject 的 - (Class)class 方法,因为隐藏参数 self 为 son的实例,所以返回 Son

所以当我们调用 [self class] 和 [super class] 方法返回的都是 Son。

加入我们在Father中重载 -(Class)class 方法

1
2
3
- (Class)class {
return NSClassFromString(@"Father")
}

那么得到的输出将变成 Son 和 Father!

如何使用JTForm



JTForm是一个能简单快速的搭建流畅复杂表单的库,灵感来自于XLFormTexture。JTForm能帮助你像html一样创建表单。不同于XLForm是一个UIViewController的子类,JTFormUIView的子类,也就是说,你可以像使用UIView一样使用JTForm,应用范围更广,更方便。JTForm也可以用来创建列表,而不仅仅是表单。

JTForm使用Texture完成视图的布局与加载,所以集成了Texture的优点:异步渲染,极度流畅。使用JTForm,你可以忘记许多原生控件时需要注意的东西:高度设置,单元行复用等。为了避免ASTableNode重载时图片闪烁的问题,自定义了JTNetworkImageNode代替ASNetworkImageNode

下面是demo运行在公司老旧设备5s的截图,可以看到fps基本保持在60左右。

fps基本保持在60
text输入表单

安装

测试一下

  • 使用cocoapods:pod 'JTForm', '~> 0.0.1'

注意事项

  • 如果库自带的单元行满足不了需求,需要自定义单元行的时候,需要了解Texture的相关知识。
  • 如果你的项目中有类似‎IQKeyboardManager的第三方,请在使用JTForm的时候禁用他们,不然会跟库的键盘弹起相冲突。如果你想禁用JTForm的键盘弹起,你可以设置JTForm的属性showInputAccessoryView为NO

简单使用

简单的表单1

下面是构建该表单一部分的代码以及注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 构建表描述
JTFormDescriptor *formDescriptor = [JTFormDescriptor formDescriptor];
// 是否在必填行的title前面添加一个红色的*
formDescriptor.addAsteriskToRequiredRowsTitle = YES;
JTSectionDescriptor *section = nil;
JTRowDescriptor *row = nil;

#pragma mark - float text
// 创建节描述
section = [JTSectionDescriptor formSection];
// 为section创建header title,目前需要手动输入header view的height
section.headerAttributedString = [NSAttributedString attributedStringWithString:@"float text" font:nil color:nil firstWordColor:nil];
// 目前需要手动输入header view的height,不然是默认值,可能会出现排版显示问题
section.headerHeight = 30.;
// 将节描述添加到表描述中
[formDescriptor addFormSection:section];

// 创建行描述,rowType为必填项,创建单元行时根据rowType来选择创建不同的单元行
row = [JTRowDescriptor formRowDescriptorWithTag:JTFormRowTypeFloatText rowType:JTFormRowTypeFloatText title:@"测试"];
// 是否必填
row.required = YES;
// 将行描述添加到表描述中
[section addFormRow:row];

#pragma mark - formatter

row = [JTRowDescriptor formRowDescriptorWithTag:@"20" rowType:JTFormRowTypeNumber title:@"百分比"];
NSNumberFormatter *numberFormatter = [NSNumberFormatter new];
numberFormatter.numberStyle = NSNumberFormatterPercentStyle;
// 添加valueFormatter,是NSFormatter的子类,能将value转换成不同的文本。常用的有nsdateformatter
// 这里valueFormatter的作用是将数字转换成百分数,例如10->1000%
row.valueFormatter = numberFormatter;
row.value = @(100);
row.required = YES;
// 在title前面添加图片
row.image = [UIImage imageNamed:@"jt_money"];
[section addFormRow:row];

row = [JTRowDescriptor formRowDescriptorWithTag:@"21" rowType:JTFormRowTypeNumber title:@"人民币"];
NSNumberFormatter *numberFormatter1 = [NSNumberFormatter new];
numberFormatter1.numberStyle = NSNumberFormatterCurrencyStyle;
// 这里valueFormatter的作用是将数字转换成货币,例如10->¥10
row.valueFormatter = numberFormatter1;
row.value = @(100);
row.required = YES;
row.image = [UIImage imageNamed:@"jt_money"];
[section addFormRow:row];

#pragma mark - common


row = [JTRowDescriptor formRowDescriptorWithTag:JTFormRowTypeName rowType:JTFormRowTypeName title:@"JTFormRowTypeName"];
// 占位符
row.placeHolder = @"请输入姓名...";
// 赋值
row.value = @"djdjd";
row.required = YES;
[section addFormRow:row];

// 创建JTForm,formDescriptor不能为空
JTForm *form = [[JTForm alloc] initWithFormDescriptor:formDescriptor];
form.frame = CGRectMake(0, 0, kJTScreenWidth, kJTScreenHeight-64.);
[self.view addSubview:form];
self.form = form;

行描述 JTRowDescriptor

行描述JTRowDescriptor是单元行的数据源,我们通过修改行描述来控制着单元行的行为,例如:是否显示,是否可编辑,高度。
下面是JTRowDescriptor的主要属性和常用方法

configMode

配置模型。

  • titleColor:标题颜色
  • contentColor:详情颜色
  • placeHolderColor:占位符颜色
  • disabledTitleColor:禁用时标题颜色
  • disabledContentColor:禁用时详情颜色
  • bgColor:控件背景颜色
  • titleFont:标题字体
  • contentFont:详情字体
  • placeHlderFont:占位符字体
  • disabledTitleFont:禁用时标题字体
  • disabledContentFont:禁用时详情字体

JTSectionDescriptorJTFormDescriptor同样具有这些属性,作用也类似。优先级JTRowDescriptor > JTSectionDescriptor > JTFormDescriptor

image & imageUrl

用于加载图片,样式类似于UITableViewCell的imageView。image应用于静态图片,imageUrl用于加载网络图片。

rowType

创建表单时,根据rowType来创建不同类型的单元行。目前库自带的rowType都已经添加到了[JTForm cellClassesForRowTypes]字典中,其中rowType为key,单元行类型Class为value。在创建时单元行时,你就可以通过字典根据rowType得到相应单元行的Class。

所以当你自定义单元行时,你需要在+ (void)load中,将相应的rowType以及对应的Class添加到[JTForm cellClassesForRowTypes]字典中。

tag

nullable,若不为空,表单将其添加到字典中,其中key为tag,value为JTRowDescriptor实例。所以如果创建表单时有多个行描述tag值一样的话,字典中将只会保存最后添加进去的JTRowDescriptor。

你可以在表单中,根据tag值找到相对应的行描述。且在获取整个表单值的时候也会派上用场。

height

该属性控制着单元行高度。默认值为JTFormUnspecifiedCellHeight,即不指定高度(自动调节高度)。

单元行高度的优先级:

  • JTRowDescriptor的height属性
  • JTBaseCellDelegate的方法+ (CGFloat)formCellHeightForRowDescriptor:(JTRowDescriptor *)row;
  • 自动调节高度

action

响应事件,目前仅用于点击单元行。如果单元行上有多个控件有响应事件时,建议使用- (JTBaseCell *)cellInForm;得到当前的单元行cell,然后用[cell.button addTarget:self action:action forControlEvents:UIControlEvents]添加响应事件。

hidden & disabled

hidden:bool值,控制隐藏或者显示当前单元行
disabled:bool值,控制当前单元行是否接受响应事件

JTSectionDescriptorJTFormDescriptor同样具有这些属性,作用也类似。优先级JTRowDescriptor > JTSectionDescriptor > JTFormDescriptor

cellConfigAfterUpdate & cellConfigWhenDisabled & cellConfigAtConfigure & cellDataDictionary

  • cellConfigAfterUpdate:配置cell,在‘update’方法后使用
  • cellConfigWhenDisabled:配置cell,当’update’方法后,且disabled属性为Yes时被使用
  • cellConfigAtConfigure:配置cell,当cell调用config之后,update方法之前调用
  • cellDataDictionary:预留,你可以选择使用时机

text

文本方面的,属性比较多,统一放到这里讲

  • valueFormatter:文本格式转换,可以将数据格式化为一种易读的格式。‘NSFormatter’是一个抽象类,我们只使用它的子类,类似’NSDateFormatter’和‘NSNumberFormatter’
  • placeHolder:占位符,当value为空时显示该内容
  • maxNumberOfCharacters:文本类单元行能输入最大字符数
  • - (nullable NSString *)displayContentValue;:在未编辑状态时,详情的显示内容
  • - (nullable NSString *)editTextValue;:在编辑状态时,详情的显示内容

验证器

你可以通过- (void)addValidator:(nonnull id<JTFormValidateProtocol>)validator;添加一个或多个验证器,验证器的作用是对单元行的值进行验证,来判断是否符合你的要求,例如:身份证格式,密码的复杂程度,字数长度等。

当然,除了库自带的验证器外,你可以自定义自己的验证器,注意需要实现代理JTFormValidateProtocol

单元行类型

文本类

  • JTFormRowTypeFloatText
  • JTFormRowTypeText
  • JTFormRowTypeName
  • JTFormRowTypeEmail
  • JTFormRowTypeNumber
  • JTFormRowTypeInteger
  • JTFormRowTypeDecimal
  • JTFormRowTypePassword
  • JTFormRowTypePhone
  • JTFormRowTypeURL
  • JTFormRowTypeTextView
  • JTFormRowTypeInfo

主要的区别是键盘不同,需要注意的是:JTFormRowTypeTextViewJTFormRowTypeInfotextview,而其它几种是textfield

text

select类

  • JTFormRowTypePushSelect

push到另一个vc中,仅可选择一个

  • JTFormRowTypeMultipleSelect

push到另一个vc中,可选择多个

  • JTFormRowTypeSheetSelect

UIAlertController,样式为UIAlertControllerStyleActionSheet

  • JTFormRowTypeAlertSelect

UIAlertController,样式为UIAlertControllerStyleAlert

  • JTFormRowTypePickerSelect

类似于弹出键盘,inputview为UIPickeraaa

选择项通常会拥有一个展示文本,一个是代表value的id。例如你在选择汽车型号的时候,展示给你的是不同汽车的型号的文本,当你选中之后传给后台的是代表该型号的文本。

在选择类的单元行中,我们使用的选择项类型是JTOptionObject,主要由两个属性formDisplayTextformValue,含义顾名思义。选择项可以通过selectorOptions赋值得到,在单元行选中之后,单元行的value也是JTOptionObject类型(单选)或者为NSArray<JTOptionObject *> *类型(多选),你可以使用NSObject类目方法- (id)cellValue;得到value。

date类

  • JTFormRowTypeDate
  • JTFormRowTypeTime
  • JTFormRowTypeDateTime
  • JTFormRowTypeCountDownTimer
  • JTFormRowTypeDateInline

除了JTFormRowTypeDateInline,其余集中的区别只是UIDatePickertimeStyletimeStyle的区别。JTFormRowTypeDateInline的效果如下:

JTFormRowTypeDateInline

其它

  • JTFormRowTypeSwitch
  • JTFormRowTypeCheck
  • JTFormRowTypeStepCounter
  • JTFormRowTypeSegmentedControl
  • JTFormRowTypeSlider

具体样式可以看demo

JTBaseCell

单元行的基类,如果你需要自定义单元行的话需要继承它。JTBaseCell里面的属性和方法都比较简单,需要注意的是JTBaseCellDelegate,下面来我来说明一下它的几个方法:

config

required。初始化控件,在这个方法里只需要创建需要的控件,但不需要为控件添加内容,因为这个时候并没有添加进去数据源JTRowDescriptor。在生命周期内该方法只会被调用一次,除非调用JTRowDescriptor的方法reloadCell,该方法会重新创建单元行。

子类中实现时需要调用[super config]

update

required。更新视图内容,在生命周期中会被多次调用。在这个方法中,我们可以为已经创建好的内容添加内容。

子类中实现时需要调用[super update]

其它

剩下的几个方法都是@optional

  • + (CGFloat)formCellHeightForRowDescriptor:(JTRowDescriptor *)row

指定单元行的高度

  • - (BOOL)formCellCanBecomeFirstResponder

指示单元行是否能够成为第一响应者, 默认返回NO

  • - (BOOL)formCellBecomeFirstResponder

单元行成为第一响应者

  • - (BOOL)formCellResignFirstResponder

单元行放弃第一响应者

  • - (void)formCellDidSelected

当前的单元行被选中了

  • - (NSString *)formDescriptorHttpParameterName

为单元行设置一个参数名称。若不为空,当调用JTFormDescriptor的方法httpParameters返回的表单字典中,key为该参数名称,value为JTRowDescriptor的value。

  • - (void)formCellHighlight

单元行高亮

  • - (void)formCellUnhighlight

单元行不高亮

自定义单元行

以demo中我自定义的单元行IGCell为例。

+ (void)load

首先,你需要一个rowType来代表该行。然后在+ (void)load方法中[[JTForm cellClassesForRowTypes] setObject:self forKey:JTFormRowTypeIGCell];将rowType与单元行关联起来。

config

1
2
3
4
5
- (void)config
{
[super config];
// 你的代码
}

在这里你可以创建好控件,但不需要为控件添加内容。注意需要调用[super config];

update

1
2
3
4
5
- (void)update
{
[super update];
// 你的代码
}

在这个方法中,我们可以为已经创建好的内容添加内容。。注意需要调用[super update];

layoutSpecThatFits

- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize。在这个方法中,你需要创建好布局。对此,你需要额外学习Texture(原AsyncDisplayKit)的布局系统。

表单行为控制

hidden

当表单完成之后,你可以通过改变JTRowDescriptor,JTSectionDescriptor,JTFormDescriptor hidden的值来隐藏或者显示相应的单元行,单元节,表单。

disabled

你可以通过改变JTRowDescriptor,JTSectionDescriptor,JTFormDescriptor disabled的值来决定相应的单元行,单元节,表单是否可以被编辑。

delete row

1
2
JTSectionDescriptor *section = [JTSectionDescriptor formSection];
section.sectionOptions = JTFormSectionOptionCanDelete;

你可以这样创建节描述,就可以让单元节具有删除单元行功能。

FAQ

你也可以通过设置JTSectionDescriptorheaderHieghtheaderView或者footerHieghtfooterView属性来自定义header/footer。目前需要手动设置高度…

如何拿到表单的值

你可以通过JTForm- (NSDictionary *)formValues获取表单值。如果设置了验证器或者有必填项,可以先调用- (NSArray<NSError *> *)formValidationErrors来获取错误集合,再获取表单值进行其它操作。

如何给日期行设置最大,最小日期

你可以通过下面的代码这样设置,虽然丑陋,但是能用…

1
2
[row.cellConfigAtConfigure setObject:[NSDate date] forKey:@"minimumDate"];
[row.cellConfigAtConfigure setObject:[NSDate dateWithTimeIntervalSinceNow:(60*60*24*3)] forKey:@"maximumDate"];

如何改变cell的高度

单元行高度的优先级:

  • JTRowDescriptor的height属性
  • JTBaseCellDelegate的方法+ (CGFloat)formCellHeightForRowDescriptor:(JTRowDescriptor *)row;
  • 根据布局来生成高度

如何自定义类似于JTFormRowTypeDateInline的内联行

如果你想要创建类似JTFormRowTypeDateInline的内联行,就意味着你需要自定义两种单元行。拿JTFormRowTypeDateInline举个例子,A:JTFormDateCell,B:JTFormDateInlineCell。当你选中A时,B显示出来,再选中A,B消失。

  • 首先,创建两种单元行A, B
  • B在load方法中,还需要额外添加[[JTForm inlineRowTypesForRowTypes] setObject: A.rowType forKey:B.rowType]
  • 剩下的操作为以下代码,你可以照着写。这里简单说明以下,当你选择A时,会调用formCellCanBecomeFirstResponderformCellBecomeFirstResponder方法。随后调用canBecomeFirstResponderbecomeFirstResponder,注意这里必须调用super的方法,不然当前单元行无法成为第一响应者。在becomeFirstResponder中,我们创建B,并且添加到A后面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
- (BOOL)formCellCanBecomeFirstResponder
{
return [self canBecomeFirstResponder];
}

- (BOOL)formCellBecomeFirstResponder
{
if ([self isFirstResponder]) {
return [self resignFirstResponder];
}
return [self becomeFirstResponder];
}

- (BOOL)canBecomeFirstResponder
{
[super canBecomeFirstResponder];
return !self.rowDescriptor.disabled;
}

- (BOOL)becomeFirstResponder
{
[super becomeFirstResponder];

NSIndexPath *currentIndexPath = [self.rowDescriptor.sectionDescriptor.formDescriptor indexPathForRowDescriptor:self.rowDescriptor];
JTSectionDescriptor *section = [self.rowDescriptor.sectionDescriptor.formDescriptor.formSections objectAtIndex:currentIndexPath.section];
JTRowDescriptor *inlineRow = [JTRowDescriptor formRowDescriptorWithTag:nil rowType:JTFormRowTypeInlineDatePicker title:nil];
JTFormDateInlineCell *inlineCell = (JTFormDateInlineCell *)[inlineRow cellInForm];

NSAssert([inlineCell conformsToProtocol:@protocol(JTFormInlineCellDelegate)], @"inline cell must conform to protocol 'JTFormInlineCellDelegate'");
inlineCell.connectedRowDescriptor = self.rowDescriptor;

[section addFormRow:inlineRow afterRow:self.rowDescriptor];
[self.findForm ensureRowIsVisible:inlineRow];

BOOL result = [super becomeFirstResponder];
if (result) {
[self.findForm beginEditing:self.rowDescriptor];
}
return result;
}

- (BOOL)canResignFirstResponder
{
BOOL result = [super canResignFirstResponder];
return result;
}

- (BOOL)resignFirstResponder
{
BOOL result = [super resignFirstResponder];
if ([self.rowDescriptor.rowType isEqualToString:JTFormRowTypeDateInline]) {
NSIndexPath *currentIndexPath = [self.rowDescriptor.sectionDescriptor.formDescriptor indexPathForRowDescriptor:self.rowDescriptor];
NSIndexPath *nextRowPath = [NSIndexPath indexPathForRow:currentIndexPath.row + 1 inSection:currentIndexPath.section];
JTRowDescriptor *inlineRow = [self.rowDescriptor.sectionDescriptor.formDescriptor formRowAtIndex:nextRowPath];
if ([inlineRow.rowType isEqualToString:JTFormRowTypeInlineDatePicker]) {
[self.rowDescriptor.sectionDescriptor removeFormRow:inlineRow];
}
}
return result;
}

如何将自己的库上传到cocoapods

假设到这里你已经将你的代码上传了github上,如果没有的话就先别看了。在这里我会用自己的项目JTForm来讲解一下整个过程

生成及修改配置文件

在创建之前,你需要将你的代码提交到github上,并且为它生成一个相应版本号的tag

pod trunk register [EMAIL] [USERNAME]


当你运行pod trunk me后,发现不是如上图的结果,那么你可能需要重新登陆或者注册,也就是运行如下的命令
pod trunk register [EMAIL] [USERNAME],运行成功之后,你填的邮箱会收到一封邮件,点进去点击里面的链接就算是登录成功了。

登录

pod spec create JTForm

cd到项目中,在readme.md的位置使用pod spec create JTForm。这个命令会生成一个JTForm.podspec的文件。

编辑JTForm.podspec

你可以直接参考或者复制别的项目podspec文件里面的内容,仅需修改一些信息。你也可以看官方文档自己编辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pod::Spec.new do |s|

s.name = "JTForm"
s.version = "0.0.1"
s.summary = "简单快速的创建复杂且流畅的表单,XLForm与Texture相结合"
s.homepage = "https://github.com/kikido/JTForm"
s.license = { :type => "MIT", }
s.author = { "kikido" => "kikido1992@gmail.com" }
s.source = { :git => "https://github.com/kikido/JTForm.git", :tag => s.version.to_s }
s.source_files = "JTForm", "JTForm/**/*.{h,m}"
s.resource = 'JTForm/JTForm.bundle'
s.dependency "SDWebImage", "~> 4.4.6"
s.dependency "Texture", '~> 2.8'
s.requires_arc = true
s.ios.deployment_target = '9.0'
s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SRCROOT)/SDWebImage" }

# s.public_header_files = "Classes/**/*.h"
end

pod spec lint

验证你编辑的podspec文件是否符合规范,如果出现error的话是不可以的,warning的话是可以的,可以使用命令pod spec lint --allow-warnings来忽略所有warnings。

下面是我验证时出现的问题:

验证出错

原因是因为podspec文件里面s.ios.deployment_target = '8.0',而Texture仅支持iOS9.0以上的版本,所以我这里做了以下的修改:s.ios.deployment_target = '9.0'

重新验证,出现了以下错误:

错误1
错误2

一开始我以为我的.h直接引用了第三方的文件导致,后来才发现是因为没有暴露SDWebImage的头文件。解决办法:在podspec文件修改配置s.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SRCROOT)/SDWebImage" }
答案来源:关于组件化使用私有Pods的一些记录

重新编译,通过

pod trunk push –allow-warnings

使用pod trunk push --allow-warnings命令将自己的库发布到cocoapods上,大概8分钟左上

上传成功

pod setup

上传成功之后,使用pod search无法搜到自己的库的话,你可以这样做:

  1. 命令rm ~/Library/Caches/CocoaPods/search_index.json

完成后重新使用pod search搜索

版本升级

当你需要升级自己库版本的时候,你可以这样做:

  1. 修改podspec中s.version的版本号
  2. 提交到git,并打上相应版本号的tag
  3. 进入到podspec目录下,使用命令pod trunk push --allow-warnings更新库

更换版本

如果你对当前版本的库不满意,但是又不想升级版本号。你可以这样做:

  1. 去掉git上相应版本号的tag
  2. git上传新的内容
  3. 上传之后打上相应版本号的tag

大牛博客

分享下对我帮助比较大,且自己认为干货比较多的大牛的博客地址:

杨萧玉

孙源

MrPeak

如何创建和使用Bundle资源包

说来惭愧,虽然已经创建过好几次了,但是偶尔还是会忘记步骤,所以在这里记录一下子,方便查阅。

简单来说,bundle就是一个文件,里面包含很多资源子文件,例如图片,音频,视频等。这些子文件是静态的,不参与编译。

创建及设置

创建bundle

创建bundle项目

base sdk改成iOS样式,默认是macOS样式

设置平台版本

COMBINE_HIDPI_IMAGES设置为NO,否则打包完成之后的png图片将变为tiff格式
设置COMBINE_HIDPI_IMAGES为NO

添加多语言

  1. 添加文字文件,文件名字为Localizable.strings
    添加多语言文件

  2. 文件本地化,点击按钮后选择english
    文件本地化

  3. 添加其他语言类型,例如chinese-simplified
    添加其他语言类型

添加图片等文件资源

目前我的操作是将这些文件直接拖到项目中

如何调用bundle里面的资源

将bundle拖到项目中,并且添加到build phasesCopy Bundle Resource

图片类资源

UIImage *image = [UIImage imageNamed:@"bundleName/picName"]

bundleName: 包名
picName: 图片名

语言本地化

1
2
3
4
5
NSURL *url = [[NSBundle mainBundle] URLForResource:@"bundleName" withExtension:@"bundle"];
NSBundle *bundle = [NSBundle bundleWithURL:url];
NSString *language = [NSLocale preferredLanguages].firstObject;
bundle = [NSBundle bundleWithPath:[bundle pathForResource:language ofType:@"lproj"]];
NSString *value = [bundle localizedStringForKey:key value:key table:nil];

bundleName:包名

使用HealthKit莫名崩溃

今天在测试自己写的框架,[JYAuthorizationManager](www.baidu.com),然后在测试`HealthKit`权限的时候老是莫名的闪退,下面是系统的log:

1
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'NSHealthShareUsageDescription must be set in the app's Info.plist in order to request read authorization for the following types: HKQuantityTypeIdentifierStepCount'
原因指的是plist文件里面没有加权限描述,但其实我已经加过了。于是各种找原因,才发现了解决方法: > 健康的权限描述里面不能有*中文*。我也是醉了。。。

! HealthKit的权限描述

参考:https://www.jianshu.com/p/780521c34de9

NSError了解一哈

说来惭愧,工作以来一直没有好好了解NSError这个类。这次的话因为一些需求,准备系统的学习一下子,下面就是我大致的总结。

综述

OC中通常使用NSError对象来发出错误信号,提供错误类型以及任何潜在原因的额外信息。

Foundation和其它Cocoa框架产生的错误通常归属于NSCocoaErrorDomain错误域。NSCocoaErrorDomain中的错误状态码都是在Foundation定义好的常量。

在子类中,可以通过覆写localizedDescription方法来提供更好的本地错误描述。

NSErrorCFError的无缝转换对象。

每个NSError对象主要提供三部分的信息:

  • code 状态码
  • domain 对应的特定错误域
  • userInfo 额外的信息

code和domain

code状态码表示问题的本质,这些状态码都在一个特定的错误域中,以防重叠和混淆。

例如在NSCocoaErrorDomain中,NSFileManager访问一个不存在的文件产生的错误代码是4。而在NSPOSIXErrorDomain中,4代表中断函数错误。

系统错误域

userInfo及一些属性

作为Cocoa的惯例,userInfo是一个可以包含任意键值对的字典,无论是为了继承或降低耦合的目的, 它都不适合拿来填满各种杂七杂八的属性。在NSError这个例子中,有一些特定的键值对应着只读属性。一下是标准NSErroruserInfo的键列表

  • NSLocalizedDescriptionKey
  • NSLocalizedFailureReasonErrorKey
  • NSLocalizedRecoverySuggestionErrorKey
  • NSLocalizedRecoveryOptionsErrorKey
  • NSFilePathErrorKey
  • NSStringEncodingErrorKey
  • NSUnderlyingErrorKey
  • NSRecoveryAttempterErrorKey
  • NSHelpAnchorErrorKey

下面是常见的几个键:

localizedDescription

NSLocalizedDescriptionKey的对应值,即userInfo[NSLocalizedDescriptionKey],下同
一段本地化的错误描述。错误的主要可呈现信息。例如NSFileReadNoPermissionError:”文件XX无法打开,因为你并没有查看它的权限”。理想状况下,这个信息会指出失败的原因以及失败原因。这个值来自于NSLocalizedDescriptionKey或者是NSLocalizedFailureErrorKey + NSLocalizedFailureReasonErrorKey或者是NSLocalizedFailureErrorKey。构建描述信息的步骤如下:

  • userInfo中查找NSLocalizedDescriptionKey,如果存在则使用
  • userInfo中查找NSLocalizedFailureErrorKey,如果存在,则使用,与NSLocalizedFailureReasonErrorKey的值组合使用(如果存在)
  • userInfoValueProvider中查找NSLocalizedDescriptionKey,如果存在则使用
  • userInfoValueProvider中查找NSLocalizedFailureErrorKey,如果存在则使用,与NSLocalizedFailureReasonErrorKey的值组合使用(如果存在)
  • userInfo或者userInfoValueProvider查找NSLocalizedFailureErrorKey,如果存在则使用

上文中提到的userInfoValueProvider我也不知道是什么东西,我猜测可能是NSError的类方法+ (id _Nullable (^)(NSError * _Nonnull, NSErrorUserInfoKey _Nonnull))userInfoValueProviderForDomain:(NSErrorDomain)errorDomain; 中的返回值

localizedRecoverySuggestion

NSLocalizedRecoverySuggestionErrorKey的对应值,即userInfo[NSLocalizedRecoverySuggestionErrorKey],一段该错误的恢复建议,适合在alert中显示为辅助消息。

localizedFailureReason

localizedFailureReason的对应值,即userInfo[localizedFailureReason],一段本地化的错误解释

如何使用调用系统API时返回的NSError

了解过上面的知识之后,那我们要怎样更好的使用系统返回给我们的NSError呢,下面是我自己使用的方式:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)showErrorDetail:(NSError *)error viewController:(UIViewController *)viewController
{
if (![viewController isKindOfClass:[UIViewController class]]) {
return;
}
if (![error isKindOfClass:[NSError class]] || !error) {
return;
}
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:error.localizedDescription message:error.localizedRecoverySuggestion preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"确定", nil) style:UIAlertActionStyleDefault handler:nil]];
[viewController presentViewController:alertController animated:YES completion:nil];
}

如何创建一个NSError

作为开发者,我们需要怎么样使用NSError,才能更好的传递错误信息呢?首先,我们可以按Foundation库中很多类那个样子,在一个自定义方法中定义NSError **类型的形参。然后,我们也可以定义属于自己的错误域、错误代码常量和userInfo中的Key。

1
2
3
4
5
6
7
8
9
10
// 自定义Domain
NSString *const JYCustomErrorDomain = @"JYCustomErrorDomain";
// 自定义key
NSString *const JYValidationStatusErrorKey = @"JYValidationStatusErrorKey";
// 自定义错误码
typedef NS_ENUM(NSInteger, JYCustomErrorCode)
{
JYCustomErrorCodeGen = -999,
JYCustomErrorCodeRequired = -1000
};

使用userInfo字典来创建一个NSError

1
2
3
4
5
6
7
8
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Operation was unsuccessful.", nil),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The operation timed out.", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Have you tried turning it off and on again?", nil)
};
NSError *error = [NSError errorWithDomain:JYCustomErrorDomain
code:JYCustomErrorCodeGen
userInfo:userInfo];

将错误信息包装在NSError对象中,很容易在不同对象或者上下文中进行传递。

结束

参考来源:

JYAuthorization的说明使用

综述

在iOS开发中,我们总会用到许多iOS的隐私功能,例如定位相机麦克风等。在编写这些功能代码的时候,我们都要先判断是否拥有权限,然后根据有无代码执行不同的操作,功能一多的话就会显得繁琐。为了解决这个问题,我自己编写了JYAuthorization这一个框架,旨在快速获取以及查询iOS的功能权限,将更多的精力放在业务上。

快速的获取及查询功能权限

支持的类型及要求

  • ARC
  • iOS 8.0+
  • OC

目前支持的隐私类型(如果有需要,后面会继续添加):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef NS_ENUM(NSUInteger, JYServiceType){
JYServiceTypeNone,
///|< 定位-使用应用期间
JYServiceTypeLocationWhenInUse,
///|< 定位-使用使用
JYServiceTypeLocationAlways,
///|< 通讯录
JYServiceTypeAddressBook,
///|< 日历
JYServiceTypeCalendar,
///|< 提醒
JYServiceTypeReminder,
///|< 相册
JYServiceTypePhoto,
///|< 麦克风
JYServiceTypeMicroPhone,
///|< 相机
JYServiceTypeCamera,
///|< 语音识别
JYServiceTypeSpeechRecognition,
///|< 健康
JYServiceTypeHealth
};

JYAuthorizationManager

JYAuthorizationManager是一个单例,负责查询以及保存功能权限的数据。

  1. 你可以通过类方法shareManager来创建实例
  2. 通过实例方法- (void)requestAccessToServiceType:(JYServiceType)authType completion:(void(^)(BOOL granted, NSError *error))completion可以快速的查询功能权限。error的使用下面会有说明
  3. 查询过的权限结果(除JYAuthorizationErrorNotDetermined),将被保存在私有属性authDict中。因为iOS的隐私功能权限,如果被修改过了,那么当前应用是会被强制退出的,所以当前的查询结果可以保存起来,避免重复查询
  4. 如果想要显示查询的Error,你可以调用实例方法jy_showErrorDetail:(NSError *)error viewController:(UIViewController *)viewController,效果就跟上面的gif图中的效果一样
  5. JYAuthorization支持多语言,你可以在JYAuthorization.bundle里面的Localizable.strings的文件中修改不同的提示语。
    语言国际化

NSError的使用

NSError如果有不明白的可以看我的另一篇博客NSError,在这里简单说明下:

  • error.domain为自定义的错误域JYAuthErrorDomain
  • error.code为自定义的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    typedef NS_ENUM(NSInteger, JYAuthorizationStatus){
    JYAuthorizationErrorNone = 0,
    ///|< 已授权
    JYAuthorizationErrorGranted = 1,
    ///|< 未授权
    JYAuthorizationErrorNotDetermined = -100,
    ///|< 无授权,切用户无法改变这个状态。例如,家长控制
    JYAuthorizationErrorRestricted = -101,
    ///|< 授权被拒绝
    JYAuthorizationErrorDenied = -102,
    ///|< 尚未启用该服务
    JYAuthorizationErrorUnServiced = -1000,
    ///|< 版本过低,不支持该服务
    JYAuthorizationErrorLowVersion = -2000
    };
  • error.localizedDescription:错误描述,具体信息你可以在Localizable.strings中修改
  • error.localizedRecoverySuggestion:错误恢复建议,具体信息你可以在Localizable.strings中修改
  • 如果error.userInfo[JYAuthOpenSettingKey]有值,那么在提示错误时,您可以选择点击前往,前往设置界面

accessIfNotDetermined属性

这是一个BOOL类型的值,默认为YES。值为YES的时候,当你调用- (void)requestAccessToServiceType:(JYServiceType)authType completion:(void(^)(BOOL granted, NSError *error))completion时,如果权限是JYAuthorizationErrorNotDetermined(未授权),则会直接调用方法请求权限。

dontAlertIfNotDetermined属性

这是一个BOOL类型的值,默认为YES。值为YES的时候,当你调用- (void)jy_showErrorDetail:(NSError *)error viewController:(UIViewController *)viewController时,如果erroe的code(错误码)是JYAuthorizationErrorNotDetermined(-100)的话,则不会显示错误提示。

如何使用

假设我们有一个需求:调用iOS的定位服务。那么我们可以像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)startLocationService
{
JYAuthorizationManager *authManager = [JYAuthorizationManager shareManager];
[authManager requestAccessToServiceType:JYServiceTypeLocationWhenInUse completion:^(BOOL granted, NSError * _Nonnull error) {
if (granted) {
// 有权限的话
self.locationManager = [[CLLocationManager alloc] init];
self.locationManager.delegate = self;
self.locationManager.distanceFilter = 50;
self.locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
[self.locationManager startUpdatingLocation];
} else {
// 没有权限
[authManager jy_showErrorDetail:error viewController:self];
}
}];
}

使用JYAuthorization,你不需要考虑有没有权限,是否未决定权限。在上面的例子中,如果尚未决定权限,并且error.codeJYAuthorizationErrorNotDetermined的话,会自动帮你请求权限,并且在获取权限之后,执行completion里面的回调。

当然,现在的很多应用,为了能够获取客户的权限,都会在申请权限之前,跳出一个弹框提示,说明获取权限的重要性,那么使用JYAuthorization也能够很方便的实现这个权限。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)startLocationService
{
JYAuthorizationManager *authManager = [JYAuthorizationManager shareManager];
authManager.accessIfNotDetermined = false;
[authManager requestAccessToServiceType:JYServiceTypeLocationWhenInUse completion:^(BOOL granted, NSError * _Nonnull error) {
if (granted) {
// 有权限的话
// todo...
} else {
if (error.code == JYAuthorizationErrorNotDetermined) {
// 如果尚未决定权限,跳出自己的提示页面
// 1.客户在自己的提示页面点击确定之后,修改`accessIfNotDetermined`为YES,且再次调用`- (void)requestAccessToServiceType:(JYServiceType)authType completion:(void(^)(BOOL granted, NSError *error))completion`这个方法
} else {
[authManager jy_showErrorDetail:error viewController:self];
}
}
}];
}

结束

好了,说明就到此为止了。如果有错误的话,欢迎指出~