iOS底层原理应用篇:应用@synchronized底层原理对其进行监控和优化
一、为什么要写底层原理应用的文章
目前iOS面试都离不开对底层的追问,尤其是高阶iOS的面试,技术深度层面基本集中在底层原理,性能优化,架构设计等方面,可见底层原理是高阶iOS面试的标配.
而笔者在面试中也听到一些来自面试的同学的困惑:面试造火箭(底层原理),入职拧螺丝(实现简单的业务逻辑).相信很多人都有一样的困惑,从笔者自身的经历以及身边一些优秀同学的经历笔者发现,如果咱们去的是一个业务团队,那可能的确如此,而如果咱们去的是一个架构团队,那就不一样了,如果咱们不了解一些机制或者功能底层原理,可能咱们leader下发的一些工作会毫无头绪,不知如何下手.这个系列的文章的目的也在于此
- 面向所有iOSer: 让我们在面试中更加加分,给面试官留下一个更好的印象,这个同学不但知道底层原理还能运用它来解决问题
- 面向架构iOSer: 给身在架构团队的iOS提供更多实践的案例,可以让架构同学真真切切解决自身工作中遇到的问题.
当然,由于这边主要force在原理的应用,因此对原理的详细细节以及探索过程不会过多赘述,感兴趣的同学可以移步笔者的小专栏: 《图解100个iOS底层原理》 xiaozhuanlan.com/dicengyuanl…
二、@synchronized底层原理大图
上图为笔者探索总结的@synchronized底层原理的大图,原理的探索细节和过程为这边就不再赘述了,感兴趣的可以移步 《图解原理3:无力吐槽的@synchronized原理》 xiaozhuanlan.com/topic/78325…
三、应用场景:监控并优化@synchronized锁
好了,我们来整理下需求:
- 检测出触发
@synchronized
锁的地方的频次,以及其中主线程的频次. - 检测出触发
@synchronized
锁的地方的临界区耗时,以及锁等待的耗时.
四、分析实现思路
由原理大图我们可以知道,@synchronized
锁在编译的时候会将临界区通过objc_sync_enter
和ojbc_sync_exit
包裹起来,objc_sync_enter
是个加锁操作,ojbc_sync_exit
是个解锁操作.如果我们把他们给hook了是不是可以实现我们的需求了呢?
来,说干就干,我们看看他们是啥.根据原理文章中可以看到它的定义:
int objc_sync_enter(id obj);
int objc_sync_exit(id obj);
大家注意到没有,这玩意是c++的方法,而不是oc的消息,因此method swizzle
肯定是不能用了,但是我们可以用fishhook呀,这玩意专门用来hook C的api的.
所以呀,我们只要把这两个api给hook掉应该能实现我们的需求,说干就干。fishhook的使用细节俺就不细讲了,不太了解的同学可以去github(github.com/facebook/fi…
五、实现功能
1.实现hook
先定义一下咱们要hook的api:
int (*old_objc_sync_enter)(id obj);
int new_objc_sync_enter(id obj) {
int res = old_objc_sync_enter(obj);
return res;
}
int (*old_objc_sync_exit)(id obj);
int new_objc_sync_exit(id obj) {
int res = old_objc_sync_exit(obj);
return res;
}
然后就是swizzle的代码:
int error = 0;
error = rebind_symbols((struct rebinding[1]){{"objc_sync_enter",new_objc_sync_enter,(void *)&old_objc_sync_enter}}, 1);
if(error < 0){
NSLog(@"error");
}
error = rebind_symbols((struct rebinding[1]){{"objc_sync_exit",new_objc_sync_exit,(void *)&old_objc_sync_exit}}, 1);
if(error < 0){
NSLog(@"error");
}
这样我们swizzle的能力就实现了。
2.实现触发频次
按照@synchronized
的原理要实现触发频次,我们只需要统计不同的obj
进入objc_sync_enter
的次数即可,先来一波伪代码:
int new_objc_sync_enter(id obj) {
int res = old_objc_sync_enter(obj);
//伪代码
Model *model = [字典 objectForKey:obj];
model.lockCount = model.lockCount + 1;
return res;
}
结合后面我们还要实现的其他功能,很明显这里我们需要一个集合类来存放我们的数据,这个集合以obj
为key,value呢是一个model
,这个model
里存放了我们所有需要的数据,类似被锁的次数,主线程上被锁的次数等。
细节代码我们后面会有个github仓库,这里我就只写一些核心的代码:
int new_objc_sync_enter(id obj) {
BOOL isMainThread = [NSThread isMainThread];
int res = old_objc_sync_enter(obj);
[[ZXSynchronizedMonitor sharedInstance] objc_sync_enter:obj
isMainThread:isMainThread];
return res;
}
//ZXSynchronizedMonitor类
- (void)objc_sync_enter:(id)obj
isMainThread:(BOOL)isMainThread {
//取obj的class,然后转成string作为key
NSString *key = [self _getInstanceStringId:obj];
ZXSynchronizedModel *model = [self.syncDic objectForKey:key];
if(model == nil) {
model = [[ZXSynchronizedModel alloc] init];
model.objClassName = key;
[self.syncDic setObject:model forKey:key];
}
//记录触发锁的总频次
model.lockCount = model.lockCount + 1;
//记录在主线程上触发锁的频次
if(isMainThread){
model.lockCountInMainThread = model.lockCountInMainThread + 1;
}
}
这样我们就实现了第一个需求:检测出触发@synchronized
锁的地方的频次,以及其中主线程的频次.
3.解决多线程问题
那么,问题来了,这个代码大家发现了吗,是存在问题的。什么问题呢?对,线程安全问题,多线程操作同一个变量self.syncDic
。
为什么呢?因为old_objc_sync_enter
里的锁只能让传递进去相同的obj
上保证线程安全,而self.syncDic
是操作所有的obj
.
那怎么办呢?再加个锁吗?大家可以不着急往下看,可以先思考一下。
思考时间。。。
加锁显然不行,再加个锁的话后面我们要计算的耗时肯定就不准确了。这里我们可以用队列
,把相关代码放到一个同步队列里面去:
//ZXSynchronizedMonitor类
- (void)objc_sync_enter:(id)obj
isMainThread:(BOOL)isMainThread {
//取obj的class,然后转成string作为key
NSString *key = [self _getInstanceStringId:obj];
dispatch_async(_logQueue, ^{
ZXSynchronizedModel *model = [self.syncDic objectForKey:key];
if(model == nil) {
model = [[ZXSynchronizedModel alloc] init];
model.objClassName = key;
[self.syncDic setObject:model forKey:key];
}
model.lockCount = model.lockCount + 1;
if(isMainThread){
model.lockCountInMainThread = model.lockCountInMainThread + 1;
}
});
}
这样不但没有了线程安全问题,我们后面要计算的耗时也不会被影响。
4.实现触发耗时
这里我们要实现两个能力,一个是检测临界区的耗时,一个是检测锁等待的耗时。
我们先来实现锁等待的耗时,想要检测锁等待的耗时其实我们只要知道 int res = old_objc_sync_enter(obj)
这行代码执行了多久即可,我们来改一下我们的代码:
int new_objc_sync_enter(id obj) {
NSTimeInterval before_enter_time = [[NSDate date] timeIntervalSince1970] * 1000;
BOOL isMainThread = [NSThread isMainThread];
int res = old_objc_sync_enter(obj);
NSTimeInterval after_enter_time = [[NSDate date] timeIntervalSince1970] * 1000;
//
NSTimeInterval wait_time = after_enter_time - before_enter_time;
[[ZXSynchronizedMonitor sharedInstance] objc_sync_enter:obj
enterTime:after_enter_time
waitCost:wait_time
isMainThread:isMainThread];
return res;
}
- (void)objc_sync_enter:(id)obj
enterTime:(NSTimeInterval)enterTime
waitCost:(NSTimeInterval)waitCost
isMainThread:(BOOL)isMainThread {
NSString *key = [self _getInstanceStringId:obj];
dispatch_async(_logQueue, ^{
ZXSynchronizedModel *model = [self.syncDic objectForKey:key];
if(model == nil) {
model = [[ZXSynchronizedModel alloc] init];
model.objClassName = key;
[self.syncDic setObject:model forKey:key];
}
model.lockCount = model.lockCount + 1;
if(isMainThread){
model.lockCountInMainThread = model.lockCountInMainThread + 1;
}
model.waitTime = model.waitTime + waitCost;
});
}
这里wait_time
就是我们统计的锁等待耗时了,接下去我们要检测临界区的耗时。临界区的耗时也就是加完锁
到即将解锁
的耗时,加完锁
的时机我们已经有了,也就是上文代码中的after_enter_time
,把它给存起来
- (void)objc_sync_enter:(id)obj
enterTime:(NSTimeInterval)enterTime
waitCost:(NSTimeInterval)waitCost
isMainThread:(BOOL)isMainThread {
NSString *key = [self _getInstanceStringId:obj];
dispatch_async(_logQueue, ^{
ZXSynchronizedModel *model = [self.syncDic objectForKey:key];
if(model == nil) {
model = [[ZXSynchronizedModel alloc] init];
model.objClassName = key;
[self.syncDic setObject:model forKey:key];
}
model.lockCount = model.lockCount + 1;
if(isMainThread){
model.lockCountInMainThread = model.lockCountInMainThread + 1;
}
model.waitTime = model.waitTime + waitCost;
//把它给存起来
[model.enterTimes addObject:@(enterTime)];
});
}
现在我们只差一个即将解锁
的时机,这时我们需要把精力放到这里了:
int new_objc_sync_exit(id obj) {
int res = old_objc_sync_exit(obj);
return res;
}
同样的我们把时间戳怼上:
int new_objc_sync_exit(id obj) {
NSTimeInterval before_exit_time = [[NSDate date] timeIntervalSince1970] * 1000;
int res = old_objc_sync_exit(obj);
[[ZXSynchronizedMonitor sharedInstance] objc_sync_exit:obj exitTime:before_exit_time];
return res;
}
- (void)objc_sync_exit:(id)obj
exitTime:(NSTimeInterval)exitTime {
NSString *key = [self _getInstanceStringId:obj];
dispatch_async(_logQueue, ^{
ZXSynchronizedModel *model = [self.syncDic objectForKey:key];
if(model == nil){
return;
}
[model.exitTimes addObject:@(exitTime)];
});
}
这样我们两个时机的时间戳都有了,接下去就只要计算即可:
NSMutableArray *res = [[NSMutableArray alloc] init];
NSArray *keys = [self.syncDic allKeys];
for (NSString *key in keys) {
ZXSynchronizedModel *model = self.syncDic[key];
NSInteger count = MIN([model.enterTimes count], [model.exitTimes count]);
for (NSInteger i = 0; i < count; i++) {
uint64_t enter = [[model.enterTimes objectAtIndex:i] unsignedIntegerValue];
uint64_t exit = [[model.exitTimes objectAtIndex:i] unsignedIntegerValue];
NSTimeInterval cost_time = exit - enter;
cost_time = cost_time + model.costTime;
model.costTime = cost_time;
}
[res addObject:model];
}
六、代码工程
笔者将整个工程的代码全部上传到了github,感兴趣的同学可以移步下载。
七、底层原理
如果你对@synchronize
的底层原理细节感兴趣,或者对iOS的其他内容的底层细节感兴趣,或者你正在为面试做准备,可以移步笔者的专栏:图解100个iOS底层原理,我会通过图解的方式逐步为你拨开一个个iOS底层的实现原理,更会将钻研的步骤详尽的展现给大家。授人以鱼,更授人以渔。
转载自:https://juejin.cn/post/7123895856134094885