su


  • 首页

  • 归档

  • 标签

从一个crash体验strong的延迟释放

发表于 2019-05-21

文章的标题看上去有点唬人, 毕竟strong作为iOS开发中一个基础的不能再基础的语义声明, 有什么好讲的…

事实上本文源自于前文从一个crash理解weak的延迟释放写完之后与小伙伴的讨论, 既然如此我们先简单回顾一下.

背景

在前文中, 我们在文章末尾给出了一个demo, 模拟笔者在项目中的crash.

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
@interface ViewController ()
@property (nonatomic, strong) Manager *strongManager;
@property (nonatomic, strong) Operation *strongOperation;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[btn setBackgroundColor:[UIColor redColor]];
[btn addTarget:self action:@selector(btnAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:btn];
// 模拟网络请求
self.strongManager = [Manager new];
__weak typeof(self) weakSelf = self;
self.strongManager.endBlock = ^{
weakSelf.strongManager = nil;
};

self.strongOperation = [Operation new];
[self.strongOperation setTarget:self.strongManager selector:@selector(testMethod)];
}

- (void)btnAction:(id)sender{
[self.strongOperation performCallBack]; // <- 1
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface Manager : NSObject
@property (nonatomic, copy) void(^endBlock)(void);

- (void)testMethod;
@end

@implementation Manager
- (void)testMethod{
if (self.endBlock) {
self.endBlock();
}
NSLog(@"%@", self);
}

- (void)dealloc{
NSLog(@"dealloc");
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface Operation : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign)SEL finishSelector;
- (void)setTarget:(id)target selector:(SEL)selector;

- (void)performCallBack;
@end

@implementation Operation
- (void)setTarget:(id)target selector:(SEL)selector{
self.target = target;
self.finishSelector = selector;
}

- (void)performCallBack{
NSMethodSignature *signature = [self.target methodSignatureForSelector:self.finishSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self.target];
[invocation setSelector:self.finishSelector];
[invocation invoke];
}
@end

在位置1处模拟网络请求回来进行回调, 然后crash, 原因如前文所说这是没有任何问题的. 然而前文中还有一张图
野指针
可以看到上面给出的demo其实是对图里的过程进行了简化, 所以来看一看正常按照图上写的话demo应该是什么样子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface ViewController ()

@property (nonatomic, strong) Manager *manager;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
//正常写代码的时候还是推荐使用self.debugObj这种形式, 这里是为了排除干扰因素于是全部是用ivar的方式
_manager = [Manager new];
__weak typeof(self) weakSelf = self;
_manager.testBlock = ^{
weakSelf.manager = nil;
};
//发起网络请求
[self.manager networkRequest]; //<- 2
}

@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Manager
.h略过

@implementation Manager

- (void)dealloc {
NSLog(@"dealloc");
}

- (void)networkRequest {
Operation *operation = [Operation new];
operation.target = self;
operation.selector = @selector(networkCallBack);
[operation performCallback];
}
// call back
- (void)networkCallBack {
if (self.testBlock) {
self.testBlock();
}
NSLog(@"%@", self);
}

@end
1
2
Operation .h .m
和文章开头demo中的operation一样

这个demo在代码2的位置模拟网络请求, 网络请求封装在manager中, 事实上, 项目代码里的网络请求也是这么写的, 唯一的不一样地方在于networkCallBack方法在这里是同步调用, 而正常项目中网络请求是异步回来的. 然后有意思的地方就来了, 可能大家都能猜到, 上面这个demo中这样并不会crash…

追根溯源

于是开启大家来找茬模式, 仔细比对两份代码不同的地方, demo其实只做了一件事,在代码位置2处通过mananger的getter方法来获取对象然后调用networkRequest方法, 而networkRequest中一系列操作会将manager销毁.
在位置2处尝试把self.manager改为_manager, 成功的触发野指针crash.
于是笔者一开始很容易的就认为, self.manager所调用的getter方法在return的时候为返回的对象加了autorelease操作, 导致manager对象在置为nil后还能存活
然鹅, 在这个demo中, manager对象dealloc的时候调用栈里并没有autorelease销毁的调用栈…所以上面那个结论其实是不成立的.
于是想到看一看这两种调用到底有什么差别, 把这两种写法的汇编拉出来看一看:
property getter
property ivar
第一张图是通过getter拿到对象的写法, 可以清晰的看到这种写法多了_objc_retainAutoreleasedReturnValue和_objc_release两个操作.
而getter方法中实际上的操作是:
manager getter
之前笔者认为getter方法在return的时候会调用objc_autoreleaseRturnValue, 可是这里很清楚显示调用的是_objc_retainAutoreleaseReturnValue, 实际上, 如果没有重写getter方法的话, 这句话都不会加.
那么问题就很简单了, 看一看这几个操作的作用是什么就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Prepare a value at +0 for return through a +0 autoreleasing convention.
id
objc_retainAutoreleaseReturnValue(id obj)
{
if (prepareOptimizedReturn(ReturnAtPlus0)) return obj;

// not objc_autoreleaseReturnValue(objc_retain(obj))
// because we don't need another optimization attempt
return objc_retainAutoreleaseAndReturn(obj);
}

// Accept a value returned through a +0 autoreleasing convention for use at +1.
id
objc_retainAutoreleasedReturnValue(id obj)
{
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

return objc_retain(obj);
}

不同版本里的runtime里对这几个函数的实现可能不太一样, 但是大概的意思是一样的, 这里摘抄的版本是objc4-709. 这里关键的函数objc_retainAutoreleasedReturnValue也就是前文中使用getter方式比ivar方式多的两部操作之一里面大概做了这么一件事, 它会与objc_autoreleaseReturnValue配合, 如果在返回值身上调用objc_autoreleaseReturnValue, 这个返回值会先被存储在TLS中, 然后外部接收方调用objc_retainAutoreleasedReturnValue时, 发现TLS中正好存了这个对象便直接返回这个对象, 从而减去存入autorelesepool的过程, 这一TLS优化在《Objective-C高级编程 iOS与OS X多线程和内存管理》一书中有一定篇幅的解释.
我们这里则是走入了没有优化的分支语句中, 也就是return objc_retain(obj);, 然后在_objc_msgSend之后调用的_objc_release. 这下就全明白了, 也就是说使用getter方法进行对象方法调用的时候, 编译器和runtime会帮我们悄咪咪的把对象引用计数+1, 在方法调用结束以后再减一. 这样一来我们这个方法在同步调用的时候就不用担心在同步的调用中因为对象被释放而引发野指针, 因为就算当前同步调用中当前对象实际持有者‘释放’了它, 在方法调用结束前还有runtime悄悄的给它+1s, 也就是文章标题中所说的延迟释放.
对于这样一行代码

1
[self.manager networkRequest];

实际上等同于

1
2
id temp = [self manager];
[temp networkRequest];

在我们这个例子里, 对象的持有关系是这样的:
持有关系
回到crash上面, 前面说到上面的demo中和实际项目中的区别是demo中是同步的, 而项目中是是异步的, 网络请求回调回来的时候networkRequest已经走完了, 也就是runtime中增加的引用计数已经释放了, 那么自然在实际项目中就crash了.
只聚焦这个问题的话,其实答案已经给出了, 不过依然还有很多值得探索, 例如本文提到其它几个函数里都做了些啥, 例如什么时候在return的时候会加上objc_autoreleaseReturnValue(通过汇编看是在return一个局部变量的时候). 如果重写demo里ViewController的manager getter方法的话, 又会有惊喜. 深入的话需要大量篇幅, 所以先用这张经典的图收尾吧…
Memory

总结

惯例总结一下:

  1. 警惕在对象持有的block中释放该对象本身这种代码
    1
    2
    3
    weakSelf.manager.testBlock = ^{
    weakSelf.manager = nil;
    };

出现这种代码的时候想一想在使用过程中有没有可能出现文中类似情况.

  1. 解释一段代码为什么没有问题可能比解释一段代码为什么有问题更加困难. 编译器和runtime以及Runloop为开发者的代码提供了很多的优化, 文中只是模拟了最简单的情况, 正常的项目环境中情况远复杂得多, 除了文中说到的引用计数增减以外还有autoreleasepool的运用, 所以开发过程中能遵守原本的内存管理语义就尽量遵守, 笔者所在的团队代码checkList中有一条是强调除了几个特定的方法中使用ivar方式访问变量, 其它都是用self.xxx的方式访问, 这样子的规范其实就避免了本文第二个demo中的crash.
  2. 最后给出一个简化的demo, 比上面那两个还要简化, 方便观察
    demo
  3. 本文中如果有说的不对的地方, 欢迎大家及时指出来.

参考

How does objc_retainAutoreleasedReturnValue work?
Friday Q&A 2011-09-30: Automatic Reference Counting

一个小广告

字节跳动西瓜视频团队真诚的邀请各路小伙伴的加入, 一起做有挑战, 有意义的事情, 欢迎大家投简历至gaobifan@bytedance.com

从一个crash理解weak的延迟释放

发表于 2019-05-14

crash

最近在项目内部碰到一个野指针crash, 说起来这个crash也很简单, 就是一个在声明一个对象的时候用的是assign, 然后对象释放了还继续调用改对象的方法就crash了. 先来看一个最简单的野指针示意图
野指针1
A对C对像是强持有, B对C对象的申明是assign, 当A释放了C以后, B在调用C的xxx方法就会野指针crash. 对于开发者来说, 很少会把对象申明成assign, 所以这种写法引发的crash是比较好排查的, 下面看另一种示意图.
野指针2
这里B对D是弱引用, C对D是assign引用, 笔者这里示意图里想表明的是C其实是通过B的某些调用才触发对assign对象D的方法调用, 同样的原因唯一对D强引用的A如果在这个过程中释放了D就会造成crash, 说到这里可能会问, 什么场景需要这么费劲的调用, 看上去B也可以直接对D进行调用而且weak不会造成野指针不是吗. 还原一下当时的crash场景.
野指针3
简单说就是A对象通过一个manager(或者viewModel之类的),发出一个网络请求, 图2中的B在这个场景中其实是一个网络库, manager是网络请求的delegate, 负责网络请求之后的回调, 这个网络库在设计的时候为了扩展性考虑在触发回调的时候使用了一个NSInvocation. 关键代码:

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
@interface SSHttpOperation()
...
//网络回调的target
@property(nonatomic, weak)id target;
//外部在发网络请求的时候可以随意设置回调方法, 达到扩展性
@property(nonatomic, assign)SEL didFinishSelector;
...
@end

@implementation SSHttpOperation
- (void)notifyWithResult:(NSDictionary*)result error:(NSError*)error continueWhenCancel:(BOOL)continueWhenCancel {
//网络请求回来, 进行外部业务回调
...
if(target && [target respondsToSelector:didFinishSelector]) {
NSMethodSignature *signature = [target methodSignatureForSelector:didFinishSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:target]; // 1
[invocation setSelector:didFinishSelector];
id wself = self;
[invocation setArgument:&wself atIndex:2];
[invocation setArgument:&result atIndex:3];
[invocation setArgument:&logicError atIndex:4];
[invocation setArgument:&userInfo atIndex:5];
[invocation invoke];

}
...
}
@end

网络库Operation的关键代码其实就是这个, 代码1的位置[invocation setTarget:target], 点开NSInvocation.h的头文件可以看到NSInvocation对target的申明是assign…
再简单看一下外部是怎么操作的引起crash:

1
2
3
4
5
6
7
Object A

[self.pgcActionManager changePGCAccount:mediaID likeStatus:!hasSubscribed extraTrack:extraTrack likeBlock:^(BOOL isLiked, NSError *error, NSArray<TTVUserAccount *> *recommendUsers) {
// 一堆业务操作 再block的最后把这个manager置为nil
...
self.pgcActionManager = nil; // 2
}];

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Manager


- (void)changePGCAccount:(NSString *)mediaID likeStatus:(BOOL)changeToLike likeBlock:(SSPGCActionManagerLikeBlock)likeBlock{
...
self.likeOperation = [SSHttpOperation httpOperationWithURLString:url getParameter:nil postParameter:parameterDict userInfo:userInfo];
[_likeOperation setQueuePriority:NSOperationQueuePriorityHigh];
[_likeOperation setFinishTarget:self selector:@selector(operation:finishedResult:error:userInfo:)];
[SSOperationManager addOperation:_likeOperation];
}

#pragma mark - 网络回调
- (void)operation:(SSHttpOperation*)operation finishedResult:(NSDictionary*)result error:(NSError*)error userInfo:(id)userInfo{
// 业务逻辑代码 xxx
//3
if (_likeBlock) {
_likeBlock(isFollow, followError, recommendUsersArray);
}
// 后续的业务逻辑代码
// 4
[TTVLoginManager showLoginForFollowWithResult:@"你的关注太多,登录保存一下吧" extraTrackDic:@{@"isOver":@(1)} position:self.position];
}

上面摘抄了一些关键代码, 调用顺序大概是:
Object A业务逻辑触发了[Manager changePGCAccount…]方法
-> Manager通过网络库发请求并通过NSInvocation触发回调(代码1)
-> 回调方法执行likeBlock(代码3)
-> block内Object A释放Manager(代码2), Manager dealloc方法也在此时执行
-> likeBlock执行完后由于业务需求调用了self.position(代码4), 崩擦擦.

weak

至此我们找到了crash的原因, 修改的方法有很多, 上面我们有说到Operation对Manager其实是弱引用的, 只是为了扩展性?(大概)才通过NSInvocation调用本该是自己的delagte回调, 其实如果牺牲一点扩展性在现在代码的使用NSInvocation的位置(代码1)替换成[self.target xxxx]就不会出问题了, 事实上现在我们大部分的代理在使用过程中也是这个思路.
事实证明,直接调确实是没有问题的, 并且有一个有意思的现象, 通过这种调用在(代码2)self.pgcActionManager = nil;的时候, 这个Manager没有立即走dealloc, 也就是并没有立即释放, 换句话说, 还有别人在持有这个Manager? 一个常识是weak修饰的对象会在对象释放之后置为nil, 但是这和被修饰对象不立即释放并没有什么关系.
于是想到了《Objective-C高级编程 iOS与OS X多线程和内存管理》一书里提到过的,weak会将被修饰对象加入autoreleasePool达到延迟释放的效果, 笔者在读这一段的时候做过诸多实验, 最后的结论是对这个说法表示怀疑, 但是现在看确实是有延迟释放的效果. 直接看答案吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
id __weak obj1 = obj;
NSLog(@"%@", obj1);
}
/* iOS5及之前编译器做法 */
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(&obj1);
objc_autorelease(tmp);//错误!!!mistake
NSLog(@"%@", tmp);
objc_destroyWeak(&obj1);

/* 现在的编译器做法*/
id obj = objc_msgSend(NSObject, "new");
id obj1;
objc_initWeak(&obj1, obj);
id tmp = objc_loadWeakRetained(obj1);//objc_loadWeakRetained would increment the reference count to ensure that tmp is alive in the NSLog statement.
NSLog(@"%@", obj1);
objc_release(tmp);
objc_destroyWeak(&obj1);
objc_storeStrong(&obj, 0);//release

现在的编译器在我们使用weak对象的时候会帮我们插入objc_loadWeakRetained导致weak对象引用计数+1, 达到在使用weak对象的时候改对象永远不可能被释放, 在我们的case中就是[self.target xxxx]这个xxx方法中即使将有代码将target释放了, 表面上看引用计数好像为0了, 应该释放, 事实上只有在这个方法走完了引用计数才有可能清0. 以前知道这个实现, 但是没注意这个特性, 现在看只想说奈斯 兄dei.

最后

最后总结一下这次改bug的收获:

  1. NSInvocation是有可能引起野指针的, 使用的时候要多加小心, 尤其是在基础库中的使用.当然就正常使用规范来说文中的那个likeBlock应该放在方法末尾执行(这样也不会crash), 只是业务需要在block执行后继续执行一些操作.
  2. weak会通过objc_loadWeakRetained函数被修饰对象引用计数+1, 保证对象在正在使用的过程中不被释放, 这也是和assign非常不一样一个点.
  3. 一些代码设计上的收获, 例如在设计基础库的时候对扩展性和稳定性的取舍, 不展开了.

demo

最后的最后

撰写本文的过程中尝试过用clang命令rewrite源码, 期间由于没有运行时环境支持开始一直报错, 现在附上一个iOS系统下的clang命令, 供有兴趣的读者参考:
xcrun -sdk iphonesimulator clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mios-version-min=12.1 -fobjc-runtime=ios-12.1 -Wno-deprecated-declarations ViewController.m

参考:

Objective-C高级编程
Why __weak object will be added to autorelease pool?

一个小广告

字节跳动西瓜视频团队真诚的邀请各路小伙伴的加入, 一起做有挑战, 有意义的事情, 欢迎大家投简历至gaobifan@bytedance.com

带着问题看源码----子线程AutoRelease对象何时释放

发表于 2018-01-21

首先是一个常规问题,autorelease对象何时释放?答:在AutoreleasePoolPage pop的时候释放,在主线程的runloop中,有两个oberserver负责创建和清空autoreleasepool,详情可以看YY的深入理解runloop。那么子线程呢?子线程的runloop都需要手动开启,那么子线程中使用autorelease对象会内存泄漏吗,如果不会又是什么时候释放呢。

Runloop源码

带着这个问题,我们看一看runloop的源码中给出的答案。

autoreleasepool创建

在MRC下,使用__autoreleasing修饰符等同于MRC下调用autorelease方法,所以在NSObject源码中找到-(id)autorelese方法开始看。

1
2
3
4
-(id) autorelease
{
return _objc_rootAutorelease(self);
}

可以看到这个方法里只是简单的调了一下_objc_rootAutorelease(),继续跟进。

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
id
_objc_rootAutorelease(id obj)
{
assert(obj);
return obj->rootAutorelease();
}

...

// Base autorelease implementation, ignoring overrides.
inline id
objc_object::rootAutorelease()
{
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

return rootAutorelease2();
}

...

__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}

检查是否AutoreleasePoolPage::autorelease是标签指针和是否要做不加入autoreleasepool的优化,然后rootAutorelease2()。最后走入了AutoreleasePoolPage::autorelease()。
接下来看看AutoreleasePoolPage这个类,有关这个类的说明,可以看看sunny的黑幕背后的Autorelease。现在来看看AutoreleasePoolPage中的实现。

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
public:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
...
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
...
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());

bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
// We are pushing a second pool over the empty placeholder pool
// or pushing the first object into the empty placeholder pool.
// Before doing that, push a pool boundary on behalf of the pool
// that is currently represented by the empty placeholder.
pushExtraBoundary = true;
}
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: (%p) Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// We are pushing a pool with no pool in place,
// and alloc-per-pool debugging was not requested.
// Install and return the empty pool placeholder.
return setEmptyPoolPlaceholder();
}

// We are pushing an object or a non-placeholder'd pool.

// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);

// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}

// Push the requested object or pool.
return page->add(obj);
}

这里我们找到了我们想看的代码,如果当前线程没有AutorelesepoolPage的话,代码执行顺序为autorelease -> autoreleaseFast -> autoreleaseNoPage。
在autoreleaseNoPage方法中,会创建一个hotPage,然后调用page->add(obj)。也就是说即使这个线程没有AutorelesepoolPage,使用了autorelease对象时也会new一个AutoreleasepoolPage出来管理autorelese对象,不用担心内存泄漏。

何时释放

明确了何时创建autoreleasepool以后就自然而然的有下一个问题,这个autoreleasepool何时清空?
对于这个问题,这里使用watchpoint set variable命令来观察。
首先是一个最简单的场景,创建一个子线程。

1
2
3
4
__weak id obj;

...
[NSThread detachNewThreadSelector:@selector(createAndConfigObserverInSecondaryThread) toTarget:self withObject:nil];

使用一个weak指针观察子线程中的autorelease对象,子线程中执行的任务。

1
2
3
4
5
6
7
- (void)createAndConfigObserverInSecondaryThread{
__autoreleasing id test = [NSObject new];
NSLog(@"obj = %@", test);
obj = test;
[[NSThread currentThread] setName:@"test runloop thread"];
NSLog(@"thread ending");
}

在obj = test处设置断点使用watchpoint set variable obj命令观察obj,可以看到obj在释放时的方法调用栈是这样的。
threadwithoutrunloop
通过这个调用栈可以看到释放的时机在_pthread_exit。然后执行到AutorelepoolPage的tls_dealloc方法。这个方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void tls_dealloc(void *p)
{
if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
// No objects or pool pages to clean up here.
return;
}

// reinstate TLS value while we work
setHotPage((AutoreleasePoolPage *)p);

if (AutoreleasePoolPage *page = coldPage()) {
if (!page->empty()) pop(page->begin()); // pop all of the pools
if (DebugMissingPools || DebugPoolAllocation) {
// pop() killed the pages already
} else {
page->kill(); // free all of the pages
}
}

// clear TLS value so TLS destruction doesn't loop
setHotPage(nil);
}

在这找到了if (!page->empty()) pop(page->begin());这句关键代码。再往上看一点,在_pthread_exit时会执行下面这个函数

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
void
_pthread_tsd_cleanup(pthread_t self)
{
#if !VARIANT_DYLD
int j;

// clean up dynamic keys first
for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
pthread_key_t k;
for (k = __pthread_tsd_start; k <= self->max_tsd_key; k++) {
_pthread_tsd_cleanup_key(self, k);
}
}

self->max_tsd_key = 0;

// clean up static keys
for (j = 0; j < PTHREAD_DESTRUCTOR_ITERATIONS; j++) {
pthread_key_t k;
for (k = __pthread_tsd_first; k <= __pthread_tsd_max; k++) {
_pthread_tsd_cleanup_key(self, k);
}
}
#endif // !VARIANT_DYLD
}

也就是说thread在退出时会释放自身资源,这个操作就包含了销毁autoreleasepool,在tls_delloc中,执行了pop操作。
这个实验本该到此就结束了,对于文章开始的问题在这里也已经有了答案,线程在销毁时会清空autoreleasepool。但是上述这个例子中的线程并没有加入runloop,只是一个一次性的线程。现在给这个线程加入runloop来看看效果会是怎么样的。

runloop source & autoreleasepool

对于runloop,我们知道runloop一定要有source才能保证run起来以后不立即结束,而source有三种,custom source,port source,timer。
先加一个timer试试

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
- (void)createAndConfigObserverInSecondaryThread{
[[NSThread currentThread] setName:@"test runloop thread"];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopAllActivities,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopRef cfrunloop = [loop getCFRunLoop];
if (observer) {

CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}
[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(testAction) userInfo:nil repeats:YES];
[loop run];
NSLog(@"thread ending");
}

- (void)testAction{
__autoreleasing id test = [NSObject new];
obj = test;
NSLog(@"obj = %@", obj);
}

这里的oberserver没有什么,就是从YYKit里复制出来的一段observer代码,用于监控runloop的状态。感兴趣的可以看看。
在testAction()中加上watchpoint断点,观察obj的释放时机。

可以看到释放的时机在CFRunloopRunSpecific中,也就是runloop切换状态的时候,继续往上看发现__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__()这个回调。这个函数中的实现如下

1
2
3
4
5
6
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
if (func) {
func(timer, info);
}
asm __volatile__(""); // thwart tail-call optimization
}

这个名为func的callback是timer的一个属性,根据这个调用栈看到,释放autoreleasepool的操作应该是在这个callback中。这里猜测一下timer,应该是在自己的callback函数里插入了释放autorelesepool的代码。
然后用自己实现的source试一试,

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
- (void)createAndConfigObserverInSecondaryThread{
__autoreleasing id test = [NSObject new];
NSLog(@"obj = %@", test);
obj = test;
[[NSThread currentThread] setName:@"test runloop thread"];
NSRunLoop *loop = [NSRunLoop currentRunLoop];
CFRunLoopObserverRef observer;
observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
kCFRunLoopAllActivities,
true, // repeat
0xFFFFFF, // after CATransaction(2000000)
YYRunLoopObserverCallBack, NULL);
CFRunLoopRef cfrunloop = [loop getCFRunLoop];
if (observer) {

CFRunLoopAddObserver(cfrunloop, observer, kCFRunLoopCommonModes);
CFRelease(observer);
}

CFRunLoopSourceRef source;
CFRunLoopSourceContext sourceContext = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, NULL, NULL, &runLoopSourcePerformRoutine};
source = CFRunLoopSourceCreate(NULL, 0, &sourceContext);
CFRunLoopAddSource(cfrunloop, source, kCFRunLoopDefaultMode);
runLoopSource = source;
runLoop = cfrunloop;
[loop run];
NSLog(@"thread ending");
}

-(void)wakeupSource{
//通知InputSource
CFRunLoopSourceSignal(runLoopSource);
//唤醒runLoop
CFRunLoopWakeUp(runLoop);

}

...

void runLoopSourcePerformRoutine (void *info)
{
__autoreleasing id test = [NSObject new];
obj = test;
// 如果不对obj赋值,obj会一直持有createAndConfigObserverInSecondaryThread函数入口的那个object,那个object不受这里面的autoreleasepool影响。
NSLog(@"obj is %@" , obj);
NSLog(@"回调方法%@",[NSThread currentThread]);
}

这里wakeupSource()是一个按钮的点击事件,用于唤醒runloop。runloop唤醒之后将执行runLoopSourcePerformRoutine函数,在runLoopSourcePerformRoutine()中观察obj的释放时机,发现是在[NSRunloop run:beforeDate:]中,查看GNU的实现

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
- (BOOL) runMode: (NSString*)mode beforeDate: (NSDate*)date
{
NSAutoreleasePool *arp = [NSAutoreleasePool new];
NSString *savedMode = _currentMode;
GSRunLoopCtxt *context;
NSDate *d;

NSAssert(mode != nil, NSInvalidArgumentException);

/* Process any pending notifications.
*/
GSPrivateNotifyASAP(mode);

/* And process any performers scheduled in the loop (eg something from
* another thread.
*/
_currentMode = mode;
context = NSMapGet(_contextMap, mode);
[self _checkPerformers: context];
_currentMode = savedMode;

/* Find out how long we can wait before first limit date.
* If there are no input sources or timers, return immediately.
*/
d = [self limitDateForMode: mode];
if (nil == d)
{
[arp drain];
return NO;
}

/* Use the earlier of the two dates we have (nil date is like distant past).
*/
if (nil == date)
{
[self acceptInputForMode: mode beforeDate: nil];
}
else
{
/* Retain the date in case the firing of a timer (or some other event)
* releases it.
*/
d = [[d earlierDate: date] copy];
[self acceptInputForMode: mode beforeDate: d];
RELEASE(d);
}

[arp drain];
return YES;
}

在GNU的实现中,targer执行相应的action操作是在[self acceptInputForMode: mode beforeDate: d];中,可以看到在runMode: (NSString*)mode beforeDate: (NSDate*)date方法中,其实是包裹了一个autoreleasepool的,也就是arp,如果在深入一些函数里面,发现其实很多地方都有autoreleasepool的函数,所以即使是我们自定义的source,执行函数中没有释放autoreleasepool的操作也不用担心,系统在各个关键入口都给我们加了这些操作。
文章到此就告一段落了,还有一种port source,也就是source1,这种source我没有去看,好奇的同学可以去看一看如果有什么不对我们一起讨论。

总结

1.子线程在使用autorelease对象时,如果没有autoreleasepool会在autoreleaseNoPage中懒加载一个出来。
2.在runloop的run:beforeDate,以及一些source的callback中,有autoreleasepool的push和pop操作,总结就是系统在很多地方都差不多autorelease的管理操作。
3.就算插入没有pop也没关系,在线程exit的时候会释放资源,执行AutoreleasePoolPage::tls_dealloc,在这里面会清空autoreleasepool。

##参考
深入理解runloop
黑幕背后的Autorelease
stackover flow

带着问题读源码----KVO

发表于 2018-01-17

面试的时候被多次问到KVO,被问起KVO的实现原理只是简单的知道会生成一个中间派生类,改类是原类的子类。然后被追问如果自己实现KVO,要怎么实现这个派生类。被观察的对象在addObserverForKey之后改对象的isa就被指向了派生类,那么[obj description]打印出来的为什么还是原来的类名。
带着这些问题,开始看KVO的源码。

KVO

KVO的源码并不是开源的,所以并不知道苹果是如何实现的,幸好还有一套GNU的实现,可以给我们提供一下思路。GNU的下载地址在这里。
对于派生类,下面代码:

1
2
obj = [[MyObject alloc] init];
[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];

当调用到最后一个方法时,runtime已经生成了一个子类,obj的isa指向新的子类,MyObject是新子类的父类。他们之间的关系如下图。

派生类关系图

现在来看看GNU中对这一块的实现。

GNU实现

在GNU的实现中,有两个关键的类:GSKVOReplacement、GSKVOBase。
从NSKeyValueObserving.m中的- (void) addObserver: (NSObject*)anObserver forKeyPath: (NSString*)aPath options: (NSKeyValueObservingOptions)options context: (void*)aContext
方法开始看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GSKVOInfo             *info;
GSKVOReplacement *r;
NSKeyValueObservationForwarder *forwarder;
NSRange dot;

setup();
[kvoLock lock];

// Use the original class
r = replacementForClass([self class]);

/*
* Get the existing observation information, creating it (and changing
* the receiver to start key-value-observing by switching its class)
* if necessary.
*/
info = (GSKVOInfo*)[self observationInfo];
if (info == nil)
{
info = [[GSKVOInfo alloc] initWithInstance: self];
[self setObservationInfo: info];
object_setClass(self, [r replacement]);
}

在这里,找到里面两行关键函数:
r = replacementForClass([self class]); 和 object_setClass(self, [r replacement]);。通过这两个函数obj就可以将自己的isa设置为runtime生成的派生类。而这个派生类体现在代码中就是[r replacement]。查看GSKVOReplacement的定义发现replacement方法返回的就是GSKVOReplacement中的一个名为replacement的Class对象。
接下来看一看这个名为r的GSKVOReplacement对象是如何生成的。看一看static GSKVOReplacement * replacementForClass(Class c)函数是如何实现的。

1
2
3
4
5
6
7
8
9
10
11
12
GSKVOReplacement *r;

setup();
[kvoLock lock];
r = (GSKVOReplacement*)NSMapGet(classTable, (void*)c);
if (r == nil)
{
r = [[GSKVOReplacement alloc] initWithClass: c];
NSMapInsert(classTable, (void*)c, (void*)r);
}
[kvoLock unlock];
return r;

在这个函数中,首先执行了静态内联函数setup()。在setup()函数中,初始化了classTable等相关的表。并且初始化了静态变量baseClass,这个baseClass是一个GSKVOBase类对象。
在replacementForClass()函数中通过传入的原Class比如MyObject,在classTable中找寻派生类比如KVO_MyObject。如果没找到,通过GSKVOReplacement的initWithClass:方法新建一个并且插入到classTable中。
那么看一看initWithClass又是如何实现的呢。initWithClass实现代码比较长,这里就贴几句关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
original = aClass;

/*
* Create subclass of the original, and override some methods
* with implementations from our abstract base class.
*/
superName = NSStringFromClass(original);
name = [@"GSKVO" stringByAppendingString: superName];
template = GSObjCMakeClass(name, superName, nil);
GSObjCAddClasses([NSArray arrayWithObject: template]);
replacement = NSClassFromString(name);
GSObjCAddClassBehavior(replacement, baseClass);

/* Create the set of setter methods overridden.
*/
keys = [NSMutableSet new];

return self;

根据代码可以看到大致的流程为:
1.通过原类拼接派生类的子类名
2.通过GSObjCAddClasses()创建新类,在该函数中,通过superName反射创建superClass,也就是MyObject的class。通过name和objc_allocateClassPair函数创建一个新的Class,也就是派生类。将生成的class对象包装在NSValue中返回给template。
3.通过GSObjCAddClasses将template里的派生类对象注册好。
4.注册好了以后就可以通过name反射得到派生类的class对象relpacement。
5.通过GSObjCAddClassBehavior(Class receiver, Class behavior)为replacement添加相应的方法。这个函数传入的两个参数分别是派生类和baseClass,也就是GSKVOBase。
GSObjCAddClassBehavior(Class receiver, Class behavior)这个函数非常关键,简单看一下其中的关键代码。

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
 unsigned int	count;
Method *methods;
Class behavior_super_class = class_getSuperclass(behavior);

...
/* 一些不是很关键的代码 */

/* Add instance methods */
methods = class_copyMethodList(behavior, &count);
BDBGPrintf(" instance methods from %s %u\n", class_getName(behavior), count);
if (methods == NULL)
{
BDBGPrintf(" none.\n");
}
else
{
GSObjCAddMethods (receiver, methods, NO);
free(methods);
}

/* Add class methods */
methods = class_copyMethodList(object_getClass(behavior), &count);
BDBGPrintf(" class methods from %s %u\n", class_getName(behavior), count);
if (methods == NULL)
{
BDBGPrintf(" none.\n");
}
else
{
GSObjCAddMethods (object_getClass(receiver), methods, NO);
free(methods);
}

/* Add behavior's superclass, if not already there. */
if (!GSObjCIsKindOf(receiver, behavior_super_class))
{
GSObjCAddClassBehavior (receiver, behavior_super_class);
}
GSFlushMethodCacheForClass (receiver);

跳过一些不那么关键的代码后其实非常简单,就是把behavior中的方法列表copy一份到receiver中。其中behavior是GSKVOBase,reveiver是派生类,也就是我们obj的isa最后指向的对象。
那么看一看GSKVOBase是如何实现的吧。
GSKVOBase有两个关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (Class) class
{
return class_getSuperclass(object_getClass(self));
}

- (void) setValue: (id)anObject forKey: (NSString*)aKey
{
Class c = [self class];
void (*imp)(id,SEL,id,id);

imp = (void (*)(id,SEL,id,id))[c instanceMethodForSelector: _cmd];

if ([[self class] automaticallyNotifiesObserversForKey: aKey])
{
[self willChangeValueForKey: aKey];
imp(self,_cmd,anObject,aKey);
[self didChangeValueForKey: aKey];
}
else
{
imp(self,_cmd,anObject,aKey);
}
}

首先重写了自己的class方法,返回的是自己的superClass,这也就是为什么KVO了obj之后[obj description]打印出来的还是MyObject信息。在setValue:forKey:方法中,通过automaticallyNotifiesObserversForKey:判断该key是否被kvo了,如果被kvo了就插入willChangeValueForKey和didChangeValueForKey两个方法。如果没有,执行原有的set方法即可。也就是说replacement中并没有重写所有的任何的set方法,而是通过这种很巧妙的操作记录一下被kvo的key,然后在该key执行set方法时,插入kvo需要的语句,这一切,都是通过runtime来进行的。
看到这里,文章开头的两个问题其实已经有答案了,GNU的这个实现其实就可以作为自己实现KVO的思路,而苹果在官方文档中关于kvo有这么一句话

1
You should never rely on the isa pointer to determine class membership. Instead, you should use the class method to determine the class of an object instance.

这么说的原因,对照GNU这里的代码看,其实就是因为重写了派生类的Class方法,所以不能依赖派生类的Class来判断类的关系。

总结

对于KVO的实现,GNU给出的这套实现中还有很多细节,但是基本的思想就是有一个模版类GSKVOBase,这个类中定义了一些基本行为,例如Class方法,setValue:forKey:方法,所有的派生类(replacement)的方法列表都来自于该模版类,这样就派生类就可以做成一个轻量级的对象,不用重写特定的set方法,在调用被KVO的对象其它方法时,也能保证调用的是原Class方法列表中的IMP,不至于出错。

参考

KVO原理浅析

KVC原理小记录

发表于 2017-09-29

无意看到KVC原理的文章,发现很多中文文章说的都不全面,翻了一下官网文档,记录一下

basic getters

基础的valueForKey:

1.搜寻实例对象存取方法,搜寻的方法名顺序为:get<Key>,<key>,is<Key>或者_<key>,如果搜寻到了类似的方法,将结果带入第5步。否则进行下一步
2.如果没有找到简单的访问器方法,开始寻找符合countOf<Key>,objectIn<Key>AtIndex:(与NSArray类的初始方法相对应地方法,参数也与NSArray的对应方法一样)和<key>AtIndexs:(与NSArray的objectsAtIndexes:对应)这种命名规则的方法。如果第一个方法(countOf<Key>)和剩下两个方法中的至少一个被发现了,创建一个能够响应所有NSArray方法的集合代理对象返回(断点调试看到的是NSKeyValueArray对象)。如果没有,进入第三步
上面创建的代理对象随后将它收到的所有NSArray相关的消息(调用)转换成创建它的那个符合kvc规则的对象中countOf<Key>,objectIn<Key>AtIndex:和<key>AtIndexs:的消息结合。如果原始的对象还实现了一个可选方法名字符合get<Key>:range:的范式,该代理对象在适当的时候也会使用改方法。实际上,这个代理对象和这个符合kvc规则的对象共同工作,允许下面的属性像一个NSArray一个呈现,即使那个属性并不是array。
3.如果简单的访问器方法和上述数组访问方法都没有,则开始搜寻三个名为countOf<Key>,enumeratorOf<Key>和memberOf<Key:>的方法(与NSSet中的原始方法相对应)
如果三个方法都实现了,创造一个能够响应所有NSSet方法的集合代理对象返回(这里没做实验)。否则进入第4步
这个代理对象随后将收到的所有NSSet消息(调用)转换成创造它的那个对象中countOf<Key>,enumeratorOf<Key>和memberOf<Key>:消息的组合。实际上,这个代理对象和这个符合kvc规则的对象共同工作,允许下面的属性像一个NSSet一个呈现,即使那个属性并不是set
4.如果上述的简单访问器方法和集合访问方法都没有找到,并且这个消息接收者的accessInstanceVariablesDirectly返回YES,那么按照_<key>,_is<Key>,<key>,或者is<Key>的顺序搜寻符合的实例变量。如果找到了,直接获取改实例变量进入第5步,如果没有,进入第6步。
5.如果返回的值是一个对象指针,直接返回
如果返回的是一个可以被包装成NSNumber的基础类型,包装成NSNumber返回
如果返回的是一个不能被包装成NSNumber的基础类型,转换成一个NSValue对象返回
6.如果上面所有的都失败了,调用valueForUndefinedKey:方法,这个方法默认会生成一个异常(crash在这方法里),不过NSObject的子类可以继承根据key做一些特定处理。

basic setter

setValue:forKey:

1.首先按顺序查找名为set<Key>:或者_set<Key>:方法,如果存在,将输入的值(或者解除封装,例如setvalue中传的一个NSNumber,set<Key>:中接收的是一个NSInteger)传入方法中调用。
2.如果简单的存取方法没找到,并且类的accessInstanceVariablesDirectly返回YES,按照_<key>,_is<Key>,<key>,或者is<Key>的名字顺序查找实例变量。如果找到了,直接设置变量值为输入值并且结束。
3.如果存取方法和实例变量都没找到,调用setValue:forUndefinedKey:方法,这个方法默认会生成一个异常,不果继承NSObject的子类可以根据key提供一些特定的处理

待续,后面还有为集合类set方法。

#参考
Key-Value Coding Programming Guide

Objective-c 动态运行时语言是什么意思

发表于 2017-07-04

看到一个题目是

1
我们说的objective-c是动态运行时语言是什么意思(when we call objective c is runtime language what does it mean)

记录一下,其实很简单,但是之前没有细致的去看官方文档说明,分两个方面:动态类型(dynamic type)和动态绑定(dynamic binding)

动态类型

动态类型指的是对象指针类型的动态性,意思就是对象的类型确定将会推迟到运行时。由赋值给它的对象类型决定对象指针的类型。

另外类型确定推迟到运行时之后,可以通过NSObject的isKindOfClass方法动态判断对象最后的类型(动态类型识别)。可以把id修饰的对象理解为动态类型对象,其他在编译器指明类型的为静态类型对象,通常如果不需要涉及到多态的话还是要尽量使用静态类型。因为出错了的话编译器可以提前查出,可读性也好。
举例说明:

1
NSString* testObject = [[NSData alloc] init];

这句话声明了一个NSString指针,但是赋值了一个NSData对象。所以在编译期下面两句话:

1
2
[testObject stringByAppendingString:@"string"];  //1
[testObject base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength]; //2

第一句话在编译期可以通过,因为编译期testObject的类型是NSString,所以第二句话会报错,因为那是NSData的方法导致程序跑不起来。
然后如果注释掉第二句话将程序run起来,程序将会在第一句话处crash,因为赋值给testObject的对象是一个NSData对象,而NSData没有stringByAppendingString方法,导致crash。

如果将testObject声明为id类型则两句话都可以编译通过。但是运行时一样会crash在stringByAppendingString,原因同上。

摘抄一段官方文档的说明:

1
2
3
4
5
6
7
8
9
10
11
12
A variable is dynamically typed when the type of the object it points to is not 
checked at compile time. Objective-C uses the id data type to represent a variable
that is an object without specifying what sort of object it is. This is referred
to as dynamic typing.

Dynamic typing contrasts with static typing, in which the system explicitly
identifies the class to which an object belongs at compile time. Static type
checking at compile time may ensure stricter data integrity, but in exchange for
that integrity, dynamic typing gives your program much greater flexibility. And
through object introspection (for example, asking a dynamically typed, anonymous
object what its class is), you can still verify the type of an object at runtime
and thus validate its suitability for a particular operation.

静态类型可以严格的保证数据完整性,动态类型牺牲了这种完整性,但是给予了更多的灵活性,例如id,而且通过对象的introspection特性,开发者依然可以在运行时验证一个对象的类型。

introspection:

1.首先是Class类型:
• Class class = [NSObject class]; // 通过类名得到对应的Class动态类型
• Class class = [obj class]; // 通过实例对象得到对应的Class动态类型
• if([obj1 class] == [obj2 class]) // 判断是不是相同类型的实例
2.Class动态类型和类名字符串的相互转换:
• NSClassFromString(@”NSObject”); // 由类名字符串得到Class动态类型
• NSStringFromClass([NSObject class]); // 由类名的动态类型得到类名字符串
• NSStringFromClass([obj class]); // 由对象的动态类型得到类名字符串
3.判断对象是否属于某种动态类型:
• -(BOOL)isKindOfClass:class // 判断某个对象是否是动态类型class的实例或其子类的实例
• -(BOOL)isMemberOfClass:class // 与isKindOfClass不同的是,这里只判断某个对象是否是class类型的实例,不放宽到其子类
4.判断类中是否有对应的方法:
• -(BOOL)respondsTosSelector:(SEL)selector // 类中是否有这个类方法
• -(BOOL)instancesRespondToSelector:(SEL)selector // 类中是否有这个实例方法
上面两个方法都可以通过类名调用,前者判断类中是否有对应的类方法(通过‘+’修饰定义的方法),后者判断类中是否有对应的实例方法(通过‘-’修饰定义的方法)。此外,前者respondsTosSelector函数还可以被类的实例对象调用,效果等同于直接用类名调用后者instancesRespondToSelector函数。 例如:

1
2
3
4
5
6
7
8
[1][Test instancesRespondToSelector:@selector(objFunc)];//YES
[2][Test instancesRespondToSelector:@selector(classFunc)];//NO

[3][Test respondsToSelector:@selector(objFunc)];//NO
[4][Test respondsToSelector:@selector(classFunc)];//YES

[5][test respondsToSelector:@selector(objFunc)];//YES
[6][test respondsToSelector:@selector(classFunc)];//NO

5.方法名字符串和SEL类型的转换
编译器会根据方法的名字和参数序列生成唯一标识改方法的ID,这个ID为SEL类型。到了运行时编译器通过SEL类型的ID来查找对应的方法,方法的名字和参数序列相同,那么它们的ID就都是相同的。另外,可以通过@select()指示符获得方法的ID。常用的方法如下:

1
2
3
SEL funcID = @select(func);// 这个注册事件回调时常用,将方法转成SEL类型
SEL funcID = NSSelectorFromString(@"func"); // 根据方法名得到方法标识
NSString *funcName = NSStringFromSelector(funcID); // 根据SEL类型得到方法名字符串

动态绑定

动态绑定指的是方法的动态性,先来看官方定义

1
2
3
4
5
6
7
8
9
10
11
Dynamic binding is determining the method to invoke at runtime instead of at 
compile time. Dynamic binding is also referred to as late binding. In Objective-C,
all methods are resolved dynamically at runtime. The exact code executed is
determined by both the method name (the selector) and the receiving object.

Dynamic binding enables polymorphism. For example, consider a collection of
objects including Dog, Athlete, and ComputerSimulation. Each object has its own
implementation of a run method. In the following code fragment, the actual code
that should be executed by the expression [anObject run] is determined at
runtime. The runtime system uses the selector for the method run to identify the
appropriate method in whatever the class of anObject turns out to be.

动态绑定的意思就是将方法的实现推迟到运行时再决定而不是编译期。在oc中,所有的方法都是运行时决定的,具体需要执行的代码需要通过方法名(selector)和接收者一起决定。

动态绑定实现了面向对象中多态这个特性。举例上面英文说的很详细了就不翻译了…代码如下

1
2
3
4
5
NSArray *anArray = [NSArray arrayWithObjects:aDog, anAthlete, aComputerSimulation, nil];

id anObject = [anArray objectAtIndex:(random()/pow(2, 31)*3)];

[anObject run];

动态绑定是基于动态类型的,在运行时对象的类型确定后,那么对象的属性和方法也就确定了(包括类中原来的属性和方法以及运行时动态新加入的属性和方法),这也就是所谓的动态绑定了。动态绑定的核心用法就该是在运行时动态的为类添加属性和方法,以及方法的最后处理或转发,主要用到C语言语法,因为涉及到运行时,因此要引入运行时头文件:objc/runtime.h。
消息转发可以参考effective oc 12条。

参考

【iOS沉思录】Objective-C语言的动态性总结(编译时与运行时)

由NSString什么时候释放说起

发表于 2017-03-01

起因

最开始只是想试一试写在方法内部的局部变量释放时经不经过autoreleasepool。
例如,下图这样的代码。
nsobject 实验
为了不影响对象本身的引用计数影响它的销毁过程,使用一个weak指针,不出所料的,打印出来了如下结果

1
2
3
2017-03-01 18:25:05.117 Hello[72930:2179247] <NSObject: 0x61000000a6e0>
2017-03-01 18:25:05.118 Hello[72930:2179247] (null)
2017-03-01 18:25:05.150 Hello[72930:2179247] (null)

但是这个实验如果换成NSString得到的则是完全不一样的结果。
如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__weak id reference = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"123456";
reference = str;
NSLog(@"%@", reference);
// str是一个autorelease对象,设置一个weak的引用来观察它
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", reference);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%@", reference);
}

打印出来的结果却是:

1
2
3
2017-03-02 10:31:41.475 Hello[75799:2289076] 123456
2017-03-02 10:31:41.475 Hello[75799:2289076] 123456
2017-03-02 10:31:41.492 Hello[75799:2289076] 123456

这个看上去也很好似乎也很好理解,NSString初始化的时候是存放在常量区的,所以没有释放嘛。

深入研究

为了观察对象的释放过程,我们在str赋值的地方加一个断点
break point
走到该断点的时候通过lldb命令watchpoint set variable str来观察,可以看到str由0x0000000000000000变成0x00000001056b3250。
赋值过程
然后一路点击Continue program execution,发现str会变成0x0000000000000000,控制台只打印了一次str的值,也就是说viewwillappear还没有执行,这点跟雷大博客(Objective-C Autorelease Pool 的实现原理)中的略不一样,我猜是apple改了。
释放1
然后看左侧的方法调用栈,会发现这个过程经过了objc_store,AutoreleasePoolPage::pop(void *)等函数通过autoreleasepool释放了。现在修改一下log语句。

1
#define Log(_var) ({ NSString *name = @#_var; NSLog(@"%@: %@ -> %p : %@ ", name, [_var class], _var, _var); })

看了几个大神之前的博客,大都还打印了retainCount,但是今天这里研究的是arc,就不打印retainCount了。
执行如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__weak id references = nil;
- (void)viewDidLoad {
[super viewDidLoad];

NSString *str = @"123456789";
NSString *bstr = [NSString stringWithFormat:@"123456789"];
NSString *cstr = [str mutableCopy];
NSNumber *xnum = [NSNumber numberWithInteger:8];
references = cstr;
self.num = xnum;
Log(str);
Log(bstr);
Log(cstr);
}

得到打印结果:

1
2
3
2017-03-02 14:25:33.478 Hello[78655:2389611] str: __NSCFConstantString -> 0x10b52c308 : 123456789 
2017-03-02 14:25:33.478 Hello[78655:2389611] bstr: NSTaggedPointerString -> 0xa1ea1f72bb30ab19 : 123456789
2017-03-02 14:25:33.478 Hello[78655:2389611] cstr: __NSCFString -> 0x608000074d00 : 123456789

可以看到这里其实是有三种String的,而references指向了cstr,此时在viewWillAppear和viewDidAppear里打印references得到的则是null。

三种String

  • NSCFConstantString: 字符串常量,放在常量区,对其retain或者release不影响它的引用计数,程序结束后释放。用字面量语法创建出来的string就是这种,所以在出了viewDidLoad方法以后在其他地方也能打印出值,压根就没释放。
1
2
Tip:
NSString *str = @“xxx”;其实创建了两个对象,指针str也是一个对象储存在栈内存中,由系统负责释放
  • NSTaggedPointerString: Tagged Point,标签指针,苹果在64位环境下对NSString和NSNumber做的一些优化,简单来说就是把对象的内容存放在了指针里,这样就不需要在堆内存里在开辟一块空间存放对象了,一般用来优化长度较小的内容。关于标签指针的内容可以参考唐巧的介绍:深入理解Tagged Pointer

    对于NSString,当非字面量的数字,英文字母字符串的长度小于等于9的时候会自动成为NSTaggedPointerString类型。代码中的bstr如果再加一位或者有中文在里面就是变成NSCFString。而NSTaggedPointerString也是不会释放的,它的内容就在本身的指针里,又没有对象释放个啥啊。所以如果把references的赋值代码改为

    1
    references = bstr;

    在viewWillAppear和viewDidAppear中也是能打印出值来的。

  • NSCFString: 这种string就和普通的对象很像了,储存在堆上,有正常的引用计数,需要程序员分配释放。所以references = cstr时,会打印出null,cstr出了方法作用域在runloop结束时就被autoreleasepool释放了。

stringWithFormat

到这里还是有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__weak id references = nil;
- (void)viewDidLoad {
[super viewDidLoad];

NSString *str = @"12345678900";
NSString *bstr = [NSString stringWithFormat:@"12345678900"];
NSString *cstr = [str mutableCopy];
references = bstr; //1
// references = cstr;//2
Log(str);
Log(bstr);
Log(cstr);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", references);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%p", references);
}

根据以上说法,当string超过10位数时,bstr和cstr都是NSCFString,可是两种情况viewWillAppear和viewDidAppear在打印的结果不一样。
bstr:

1
2
2017-03-02 17:27:12.637 Hello[81349:2493571] 12345678900
2017-03-02 17:27:13.187 Hello[81349:2493571] 0x0

cstr:

1
2
2017-03-02 17:28:40.768 Hello[81398:2494728] (null)
2017-03-02 17:28:41.311 Hello[81398:2494728] 0x0

根据太阳神黑幕背后的Autorelease中的说法,是因为viewWillAppear和viewDidLoad在一个runloop中导致bstr在willappear中能打印出来值。而cstr由于来自于mutablecopy方法,被自己持有,并不会加入到autoreleasepool当中,文章最开始用NSObject做实验时也是这个道理。

所以,stringWithFormat这个工厂方法在返回值时会先把对象加入到最近的autoreleasepool当中。
查资料得知以 alloc, copy, init,mutableCopy和new这些方法打头的方法,返回的都是 retained return value,例如[[NSString alloc] initWithFormat:],而其他的则是unretained return value,例如 [NSString stringWithFormat:]。对于前者调用者是要负责释放的,对于后者就不需要了。而且对于后者ARC会把对象的生命周期延长,这里文档中没说延长方式,我猜加入自动释放池是手段之一,确保调用者能拿到并且使用这个返回值,但是并不一定会使用 autorelease,在worst case 的情况下才可能会使用,因此调用者不能假设返回值真的就在 autorelease pool中,有的时候为了优化会直接拿到返回值,狮子头一书中有讲。从性能的角度,这种做法也是可以理解的。如果我们能够知道一个对象的生命周期最长应该有多长,也就没有必要使用 autorelease 了,直接使用 release 就可以。如果很多对象都使用 autorelease 的话,也会导致整个 pool 在 drain 的时候性能下降。

1
2
3
When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.

ARC performs no extra mandatory work on the caller side, although it may elect to do something to shorten the lifetime of the returned value.

也就是说通过工程方法得到的string生命周期被延长了,所以才会在viewWillAppear里依然可以打印出来。为了证实这一点,我们换成array来做个实验。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__weak id references = nil;
- (void)viewDidLoad {
[super viewDidLoad];
NSObject *object = [NSObject new];
NSArray *arr = @[object]; //1
//NSArray *arr = [NSArray arrayWithObjects:object, nil]; //2
references = arr;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
NSLog(@"%@", references);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"%p", references);
}

第一种情况通过字面量创建array,打印台输出:

1
2
2017-03-02 17:48:19.667 Hello[81612:2505809] (null)
2017-03-02 17:48:20.216 Hello[81612:2505809] 0x0

第二种情况通过工厂方法创建,打印台输出:

1
2
3
4
2017-03-02 17:50:04.964 Hello[81664:2507484] (
"<NSObject: 0x618000200480>"
)
2017-03-02 17:50:05.508 Hello[81664:2507484] 0x0

可以看到通过工厂方法创建的array生命周期确实被延长了。

总结

1.NSCFString跟普通对象一样是可以释放的
2.NSString和NSArray的工厂方法可以延长对象的生命周期,但不一定是通过autorelease,有可能优化(同理,NSDictionary也是一样的,有兴趣的可以试一下)
3.NSArray的释放做了很多优化,释放时机是个谜,有空总结下。

##参考资料
黑幕背后的Autorelease
Objective-C Autorelease Pool 的实现原理
Objective-C 内存管理——你需要知道的一切

Effective OC 46:不要用dispatch_current_queue

发表于 2016-09-21

结论

国际惯例先说结论:dispatch_current_queue已经被废弃了,虽然可以继续使用,但是会出问题的,比如说,你以为dispatch_current_queue返回的是queueA,但是queueA是在queueB的block中执行,外层的queueB会死锁。

原因

第一点是会造成简单的死锁,

如下代码:

dispatch_sync(queueA,^{
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{/*...*/};
        if(dispatch_get_current_queue() == queueA) {
            block();
        }else {
            dispatch_sync(queueA,block);
        }
    })
});

这段代码是有问题的,代码的原意是想要在queueA上执行这个block,但是一开始dispatch_sync的queueA又在等待block执行完,造成死锁。这种写法略装b,是为了死锁而死锁的写法,但是还有一种意象不到的情况会死锁(虽然见的也不多)。

第二点,就是结论中说的queueA是在queueB的block中执行,外层的queueB会死锁。

队列是有层级体系的,以effective oc里的举例来看(p182图6-4),队列B,C的target是A,那么B,C里的block会在队列A中串行执行,队列A,D的target是全局的并发队列,那么队列A,D里的block会并发执行,如果有多个核心可能还会开多个线程并行执行。队列的层级体系可以通过dispatch_set_target_queue来设置。

dispatch_set_target_queue官方文档的说法是:

A dispatch queue’s priority is inherited from its target queue. Use the dispatch_get_global_queue function to obtain a suitable target queue of the desired priority.

If you submit a block to a serial queue, and the serial queue’s target queue is a different serial queue, that block is not invoked concurrently with blocks submitted to the target queue or to any other queue with that same target queue.

Important

If you modify the target queue for a queue, you must be careful to avoid creating cycles in the queue hierarchy.

简单翻译过来就是:

disptach_set_target_queue两个作用:

  1. 队列(queue)的优先级是继承自他的目标队列(target queue)
  2. 如果提交一个block到串行队列,这个串行队列的目标队列是另一个串行队列,这个block不会和目标队列里的其他block并发执行,而是按照提交顺序执行。

文档里的说法其实和上文说的一样,这样的话,一个blockC如果是在队列c中执行,但是队列c又是在队列A的blockA中执行,这时blockC中get_current_queue拿到的是queueC,然后放心的去queueA中执行其它操作,但是此时queueA其实正在等待自己的blockA执行完成,而blockA又在等queueC的blockC执行完成,这个时候在queueA中执行其它操作就容易造成死锁。

最后,通过effective oc上给的例子来试验一下dispatch_set_target_queue和dispatch_get_specific。

dispatch_queue_t queueA = dispatch_queue_create("bifangao.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("bifangao.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);

static int kQueueSpecific;
CFStringRef queueSpecificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &kQueueSpecific, (void*)queueSpecificValue,     (dispatch_function_t)CFRelease);


dispatch_sync(queueB, ^{
    dispatch_block_t block = ^{
        NSLog(@"no deadlock");
    };
    CFStringRef retrievedValue = dispatch_get_specific(&kQueueSpecific);
    if (retrievedValue) {
        block();
    }else{
        dispatch_sync(queueA, block);
    }
});

如果把第三行的dispatch_set_target注释掉,在下面的dispatch_sync中会拿不到retrievedValue走else里的语句,在queueA中执行block,但是如果set了target,是可以拿到retrievedValue,在queueB中执行block的,原因在于如果没有dispatch_set_target,当前代码在哪个队列中执行,就拿到哪个队列的specific,但是如果设置了目标队列,则可以顺着队列链找到queueA,继而拿到queueA的specific,最后在queueB中执行block。
原文:

The misunderstanding here is that dispatch_get_specific doesn’t traverse the stack of nested queues, it traverses the queue targeting lineage. For instance, if you did this instead,

出处:StackOverflow

iOS旋转屏幕和坐标系

发表于 2016-07-14

坐标系

一般情况下我们手机是竖屏的,此时的坐标系如下:

默认坐标系

ok,我们知道每个view有自己的坐标系,所以如果旋转白色的view(盖在UIWindow上的view),transform180度,坐标系会变成:

旋转后的坐标系

看上去天经地义对不对。

但是如果旋转device的话…

[[UIDevice currentDevice] setValue:[NSNumber numberWithInteger:UIDeviceOrientationLandscapeLeft] forKey:@"orientation"];

可以看到屏幕会被旋转成横向,并且横向会多出空白部分,此时的坐标系:

横向

可以看到这个时候屏幕是转过来的,坐标系还是从statusbar那里开始算的,坐标系的更改会对我们的subview的位置有影响,因为subview的frame并没有变化,所以在视觉上,他们就会变一个位置。

旋转屏幕

那么旋转屏幕有什么思路,现在我们旋转上图中灰色的部分,并且让灰色部分铺满屏幕,分三种思路吧:

  1. 旋转灰色的view,白色父view不动。
  2. 旋转整个父view或者uiwindow,带动子view
  3. 旋转device就好了

需要注意的是旋转父view的话会让坐标系跟着转,子view可能会出现在你不想让它出现的位置。旋转device的话弹出的键盘会跟着旋转,其它的旋转方式则没有这个效果。
直接上代码好了。

旋转屏幕demo

隐藏statusbar

最后,一般全屏的时候statusbar都是隐藏的,分享一段代码:

- (void)showStatusBar {
 UIWindow *statusBarWindow = [(UIWindow *)[UIApplication sharedApplication] valueForKey:@"statusBarWindow"];
 CGRect frame = statusBarWindow.frame;
 frame.origin.y = 0;
 statusBarWindow.frame = frame;
}

- (void)hideStatusBar {
 UIWindow *statusBarWindow = [(UIWindow *)[UIApplication sharedApplication] valueForKey:@"statusBarWindow"];
 CGRect frame = statusBarWindow.frame;
 CGSize statuBarFrameSize = [UIApplication sharedApplication].statusBarFrame.size;
 frame.origin.y = -statuBarFrameSize.height;
 statusBarWindow.frame = frame;
}

好了今天就先到这吧。。剩下不想讲了烂尾就烂尾。。。

装好nvm以后command not found

发表于 2016-07-01

装好nvm,使用hexo搭建博客,吃个饭的功夫回来发现hexo指令不好使了,nvm也不好使了 = =,报错

1
-bash: nvm: command not found

原以为是nvm被谁卸了,使用

1
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.25.4/install.sh | bash

重新装,提示我已经装了。

oh…明白了…重跑是吧…
隧:

1
source ~/.nvm/nvm.sh

顺便检查下node版本

1
ls -a ~/.nvm/versions/node

最后我用的是4.4.5

1
nvm use v4.4.5

好了,又可以愉快的提交blog了

1
hexo clean && hexo g && hexo d

妥妥的

12
su

su

12 日志
© 2019 su
由 Hexo 强力驱动
主题 - NexT.Muse