原做于:2018-10-08 GitHub Repo:BoyangBloggit
这里将经过几道面试题来扩展知识。 这几道题有几个取自sunnyxx。github
#import <UIKit/UIKit.h> #import "AppDelegate.h" int d = 1000; // 全局变量 static int e = 10000; // 静态全局变量 int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); int a = 10; // 局部变量 static int b = 100; // 静态局部变量 __block int c = 1000; void (^block)(void) = ^{ NSLog(@"Block中--\n a = %d \n b = %d\n c = %d \n d = %d \n e = %d",a,b,c,d,e); }; a = 20; b = 200; c = 2000; d = 20000; e = 200000; NSLog(@"Block上--\n a = %d \n b = %d\n c = %d \n d = %d \n e = %d",a,b,c,d,e); block(); NSLog(@"Block下--\n a = %d \n b = %d\n c = %d \n d = %d \n e = %d",a,b,c,d,e); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } 复制代码
答案是面试
2019-04-04 04:50:58.508341+0800 Block_Test[19213:1138920] Block上-- a = 20 b = 200 c = 2000 d = 20000 e = 200000 2019-04-04 04:50:58.509229+0800 Block_Test[19213:1138920] Block中-- a = 10 b = 200 c = 2000 d = 20000 e = 200000 2019-04-04 04:50:58.509395+0800 Block_Test[19213:1138920] Block下-- a = 20 b = 200 c = 2000 d = 20000 e = 200000 复制代码
解答:express
- (void)test{ __block Foo *foo = [[Foo alloc] init]; foo.fooNum = 20; __weak Foo *weakFoo = foo; self.block = ^{ NSLog(@"block中-上 fooNum = %d",weakFoo.fooNum); [NSThread sleepForTimeInterval:1.0f]; NSLog(@"block中-下 fooNum = %d",weakFoo.fooNum); }; dispatch_async(dispatch_get_global_queue(0, 0), ^{ self.block(); }); [NSThread sleepForTimeInterval:0.2f]; NSLog(@"end"); } 复制代码
结果是swift
block中-上 fooNum = 20 end block中-下 fooNum = 0 复制代码
weakFoo是一个弱指针,因此self.block对person是弱引用。 而后在并发队列中经过异步函数添加一个任务来执行self.block();,因此是开启了一个子线程来执行这个任务,此时打印fooNum值是20,而后子线程开始睡眠1秒钟;与此同时主线程也睡眠0.2秒。 而因为foo是一个局部变量,并且self.block对它也是弱引用,因此在test函数执行完后foo对象就被释放了。再过0.8秒钟,子线程结束睡眠,此时weakFoo所指向的对象已经变成了nil,因此打印的fooNum是0。markdown
[NSThread sleepForTimeInterval:0.2f];
改成[NSThread sleepForTimeInterval:2.0f];
呢?结果是并发
block中-上 fooNum = 20 end block中-下 fooNum = 20 复制代码
由于子线程睡眠结束时主线程还在睡眠睡眠,也就是test方法还没执行完,那person对象就还存在,因此子线程睡眠先后打印的fooNum都是20。app
__strong Foo *strongFoo = weakFoo;
,并改成打印strong.fooNum呢?结果仍是:框架
block中-上 fooNum = 20 end block中-下 fooNum = 20 复制代码
__strong的做用就是保证在block中的代码块在执行的过程当中,它所修饰的对象不会被释放,即使block外面已经没有任何强指针指向这个对象了,这个对象也不会立马释放,而是等到block执行结束后再释放。因此在实际开发过程当中__weak和__strong最好是一块儿使用,避免出现block运行过程当中其弱引用的对象被释放。异步
- (void)test{ self.age = 20; self.block = ^{ NSLog(@"%d",self.age); }; self.block(); } 复制代码
答:会发生循环引用。 由于self经过一个强指针指向了block,而block内部又捕获了self并且用强指针指向self,因此self和block互相强引用对方而形成循环引用。 若是要解决的话很简单,加一个__weak typeof(self) weakSelf = self;
就好。
self.block();
呢?答: 同样会引用,同样会发生循环引用。
NSLog(@"%d",self.age);
改成NSLog(@"%d",_age);
呢?答:仍是会发生循环引用。由于_age,实际上就是self->age。
[UIView animateWithDuration:1.0f animations:^{ NSLog(@"%d",self.age); }]; dispatch_sync(dispatch_get_global_queue(0, 0), ^{ NSLog(@"%d",self.age); }); 复制代码
答:不会。这里的block其实是这个函数的一部分,是参数。虽然block强引用了self,可是self并无强引用block,因此没事。
- (void)blockProblem { __block int a = 0; void (^block)(void) = ^{ self.string = @"retain"; NSLog(@"biboyang"); NSLog(@"biboyang%d",a); }; // block();//禁止 } 复制代码
咱们能够经过如下几种方式来实现
- (void)blockProblemAnswer0:(void(^)(void))block { //动画方法 [UIView animateWithDuration:0 animations:block]; //主线程 dispatch_async(dispatch_get_main_queue(), block); } 复制代码
这里两个都是直接调用了原装block的方法。
- (void)blockProblemAnswer1:(void(^)(void))block { [[NSBlockOperation blockOperationWithBlock:block]start]; } 复制代码
直接使用NSOperation的方法去调用。注意,这个方法是在主线程上执行的。
- (void)blockProblemAnswer2:(void(^)(void))block { NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?"]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation invokeWithTarget:block]; } 复制代码
NSMethodSignature是方法签名,封装了一个方法的返回类型和参数类型,只有返回类型和参数类型。
- @? 表明了这个是一个block。
NSInvocation对象包含Objective-C消息的全部元素:目标、选择器、参数和返回值。这些元素均可以直接设置,当NSncOcObjt对象被调度时,返回值自动设置。
NSInvocation对象能够重复地分配到不一样的目标;它的参数能够在分派之间进行修改,以得到不一样的结果;甚至它的选择器也能够改变为具备相同方法签名(参数和返回类型)的另外一个。这种灵活性使得NSInvocation对于使用许多参数和变体重复消息很是有用;您没必要为每一个消息从新键入稍微不一样的表达式,而是每次在将NSInvocation对象分派到新目标以前根据须要修改NSInvocation对象。
- (void)blockProblemAnswer3:(void(^)(void))block { [block invoke]; } 复制代码
咱们经过打印,能够获取到block的继承线。
-> __NSMallocBlock__ -> __NSMallocBlock -> NSBlock -> NSObject
复制代码
而后咱们查找 NSBlock的方法
(lldb) po [NSBlock instanceMethods] <__NSArrayI 0x600003265b00>( - (id)copy, - (id)copyWithZone:({_NSZone=} *)arg0 , - (void)invoke, - (void)performAfterDelay:(double)arg0 ) 复制代码
咱们发现了一个invoke方法,这个方法实际上也是来自 NSInvocation。该方法是将接收方的消息(带参数)发送到目标并设置返回值。
注意:这个方法是NSInvocation的方法,不是Block结构体中的invoke方法。
void *pBlock = (__bridge void*)block; void (*invoke)(void *,...) = *((void **)pBlock + 2); invoke(pBlock); 复制代码
开始 (__bridge void*)block
将block转成指向block结构体第一位的指针。而后去计算偏移量。
而后观察block的内存布局
struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...); struct Block_descriptor *descriptor; /* Imported variables. */ }; 复制代码
在64位下,一个void指针占了8byte。而int占据4位,则flag和reserved一共占据了8位,加一块是16位。
咱们知道,一个 void*
占据了8位, (void **)pBlock
表明了自己的8位地址长度。+2表示添加了两倍的8位长度,也就是16位。到达了 void (*invoke)
方法。
而后咱们再调用 void (*invoke)(void *,...)
,这里是block的函数指针,直接去调用就好。
static void blockCleanUp(__strong void(^*block)(void)){
(*block)();
}
- (void)blockProblemAnswer5:(void(^)(void))block {
__strong void(^cleaner)(void) __attribute ((cleanup(blockCleanUp),unused)) = block;
}
复制代码
这里能够查看黑魔法__attribute__((cleanup))
- (void)blockProblemAnswer6:(void(^)(void))block { asm("movq -0x18(%rbp), %rdi"); asm("callq *0x10(%rax)"); } 复制代码
咱们给一个block打断点,并在lldb中输入dis查看汇编代码。
-> 0x1088c8d1e <+62>: movq -0x18(%rbp), %rax 0x1088c8d22 <+66>: movq %rax, %rsi 0x1088c8d25 <+69>: movq %rsi, %rdi 0x1088c8d28 <+72>: callq *0x10(%rax) 复制代码
注意,必定要写第一行。
不写第一行的话,若是没有拦截外部变量的话仍是没问题的,可是一旦拦截到了外部变量,就会没法肯定偏移位置而崩溃。
我最开始的思路是这样的,将block的结构替换实现出来,做为中间体用来暂存方法指针。而后一样实现替换block的结构体,用来装载。
//中间体 typedef struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }__block_impl; //接受体 typedef struct __block_impl_replace { void *isa_replace; int Flags_replace; int Reserved_replace; void *FuncPtr_replace; }__block_impl_replace; //替换方法 void hookBlockMethod() { NSLog(@"黄河入海流"); } void HookBlockToPrintHelloWorld(id block) { __block_impl_replace *ptr = (__bridge __block_impl *)block; ptr->FuncPtr_replace = &hookBlockMethod; } 复制代码
注意,结构体里的方法名不比和系统block中的方法名相同,这里这么写只不过是为了标明。 这里事实上是会触发一个警告 Incompatible pointer types initializing '__block_impl_replace *' (aka 'struct __block_impl_replace *') with an expression of type '__block_impl *' (aka 'struct __block_impl *')
警告咱们这两个方法并不兼容。实际上,这两个结构体里的方法名并不相同,甚至个数不一样均可以,可是必定要保证前四个成员的类型是对应了;前四个成员是存储block内部数据的关键。 在四个成员下边接着又其余成员也是无所谓的。
typedef struct __block_impl_replace { void *isa_replace; int Flags_replace; int Reserved_replace; void *FuncPtr_replace; void *aaa; void *bbb; void *ccc; }__block_impl_replace; 复制代码
好比这种方式,实际上方法依然成立。
固然,这种方式也是能够优化的。好比说咱们就能够吧中间结构体和替换block结合。
好比下面的这个就是优化以后的结果。
typedef struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }__block_impl; void OriginalBlock (id Or_Block) { void(^block)(void) = Or_Block; block(); } void HookBlockToPrintHelloWorld(id block) { __block_impl *ptr = (__bridge __block_impl *)block; ptr->FuncPtr = &hookBlockMethod; } ------------------ ------------------ void (^block)(void) = ^void() { NSLog(@"白日依山尽 "); }; HookBlockToPrintHelloWorld(block); block(); 复制代码
这里咱们就能够打印出来 黄河入海流
了。
可是,咱们若是想要本来的方法也也打印出来该怎么处理呢?
方法很简单
void OriginalBlock (id Or_Block) { void(^block)(void) = Or_Block; block(); } void HookBlockToPrintHelloWorld(id block) { __block_impl *ptr = (__bridge __block_impl *)block; OriginalBlock(block); ptr->FuncPtr = &hookBlockMethod; } 复制代码
保留原有block,并在该方法中执行原有的block方法。
咱们就能够实现以下了
2018-11-19 17:12:16.599362+0800 BlockBlogTest[64408:32771276] 白日依山尽 2018-11-19 17:12:16.599603+0800 BlockBlogTest[64408:32771276] 黄河入海流 复制代码
这里我参考了网上的一些讨论,并结合原有的思路,回答以下
static void (*orig_func)(void *v ,int i, NSString *str); void hookFunc_2(void *v ,int i, NSString *str) { NSLog(@"%d,%@", i, str); orig_func(v,i,str); } void HookBlockToPrintArguments(id block) { __block_impl *ptr = (__bridge __block_impl *)block; orig_func = ptr->FuncPtr; ptr->FuncPtr = &hookFunc_2; } ---------------- ---------------- void (^hookBlock)(int i,NSString *str) = ^void(int i,NSString *str){ NSLog(@"bby"); }; HookBlockToPrintArguments(hookBlock); hookBlock(1,@"biboyang"); 复制代码
这样就能够打印出来
2018-11-19 17:12:16.599730+0800 BlockBlogTest[64408:32771276] 1,biboyang 2018-11-19 17:12:16.599841+0800 BlockBlogTest[64408:32771276] bby 复制代码
第三题说实话我尚未实现出来,可是在北京参加swift大会的时候,和冬瓜讨论过这个问题。 我当时的思路是在把block提出一个父类,而后在去统一修改。可是后来冬瓜介绍了fishhook框架,个人思路就变了。 在ARC中咱们使用的都是堆block,可是建立的时候是栈block,它会通过一个copy的过程,将栈block转换成堆block,中间会有objc_retainBlock->_Block_copy->_Block_copy_internal方法链。咱们能够hook这几个方法,去修改。