iOS进阶--block详解

ios的oc语法底层是基于c语言来实现的,为了更好的了解ios的一些底层的东西,首先我们将oc转成c语言,具体方法如下。

打开终端,输入xcodebuild -showsdks

可以获取到本地上所装的SDK。


接下来cd到你要rewrite的文件的目录下,如果该文件没有依赖第三方库或者framework的话,直接

xcrun -sdk iphonesimulator11.2 clang -rewrite-objc main.m 

如果依赖framework,则

xcrun -sdk iphonesimulator11.2 clang -rewrite-objc -F /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m

此时可以获取到一个.cpp文件。打开后可以看到整个文件的底层C语言实现。

先输入一个block块代码。(block函数的语法和使用比较简单,不讲)


将其转化之后,得到


我们将viewdidload里面的方法调用取出来

static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));

    int num1 = 30,num2 =50;
    int (*myBlock)(int a) = ((int (*)(int))&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, num1, num2));
    ((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 100);
}

首先我们注意到viewDidLoad的方法传入了两个参数,这两个参数被称作隐式参数。self不多介绍,_cmd的作为一个SEL指针类型,指向某一个函数的IMP指针。

这里执行的操作就是:初始化一个block实例,交给我们这么myBlock名字变量,也就是用myBlock这个指针指向这个block实例,执行的时候,直接找到这个block中的指向函数地址的指针。int (*myBlock)(int a)这个则是C语言经典的函数指针。

再看myBlock的定义。

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  int num1;
  int num2;
  __ViewController__viewDidLoad_block_impl_0(void *fp,struct __ViewController__viewDidLoad_block_desc_0 *desc,int _num1, int _num2,int flags=0) : num1(_num1), num2(_num2) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself,int a) {
  int num1 = __cself->num1; // bound by copy
  int num2 = __cself->num2; // bound by copy

        return a+num1+num2+100;
    }

可以看到,这里对应我们代码中的block中的实现,所以可以知道,block使用的匿名函数,实际上被当作一个函数来处理。不过传入的是:一个_main_block_impl_0类型的结构体,里面有一个block_impl的结构体,和一个_main_block_desc_0的结构体。跟着是他们的构造函数。

来简单看一下这个_main_block_impl_0结构体吧:

isa指向这个block的类型。这里说明这个block是NSConcreteStackBlock类型的。

(补充:OS中有三种block,下文会细说
NSConcreteGlobalBlock;//在全局中定义的
NSConcreteStackBlock; //在局部定义的
NSConcreteMallocBlock;//分配在堆中)

flag是标志,可以看到,默认构造为0;
还有一个FuncPtr,也就是指向函数地址的指针。
还有一个__main_block_desc_0的结构体,
在下面可以看到这个结构体的初始化,
一个是reserverd默认为0,
一个是block_size。是这个impl的size。
所以,这个_main_block_impl_0,我们可以理解为就是一个block实例,里面的成员变量有要执行的函数的指针,和isa(和所有的oc对象一样),还有一个size。

此时再研究下外部参数,num1,num2。block中用到的变量被作为成员变量追加到了结构体中,而由于只是传入了num1,num2的值,在结构体内部是无法直接作修改的。

当加上了__block前缀之后,我们再来观察num1和num2发生的变化。

struct __Block_byref_num1_0 {
  void *__isa;
__Block_byref_num1_0 *__forwarding;
 int __flags;
 int __size;
 int num1;
};
struct __Block_byref_num2_1 {
  void *__isa;
__Block_byref_num2_1 *__forwarding;
 int __flags;
 int __size;
 int num2;
};

struct __ViewController__viewDidLoad_block_impl_0 {
  struct __block_impl impl;
  struct __ViewController__viewDidLoad_block_desc_0* Desc;
  __Block_byref_num1_0 *num1; // by ref
  __Block_byref_num2_1 *num2; // by ref
  __ViewController__viewDidLoad_block_impl_0(void *fp,struct __ViewController__viewDidLoad_block_desc_0 *desc, __Block_byref_num1_0 *_num1, __Block_byref_num2_1 *_num2,int flags=0) : num1(_num1->__forwarding), num2(_num2->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself,int a) {
  __Block_byref_num1_0 *num1 = __cself->num1; // bound by ref
  __Block_byref_num2_1 *num2 = __cself->num2; // bound by ref

        return a+(num1->__forwarding->num1)+(num2->__forwarding->num2)+100;
    }

原来是把一个局部变量,封装成了一个结构体!!

赋值的时候直接给这个结构体中的这个值赋值。在访问这些变量的时候,实质就是在访问它的结构体。

static int __ViewController__viewDidLoad_block_func_0(struct __ViewController__viewDidLoad_block_impl_0 *__cself,int a) {
  __Block_byref_num1_0 *num1 = __cself->num1; // bound by ref
  __Block_byref_num2_1 *num2 = __cself->num2; // bound by ref

        (num1->__forwarding->num1) = 1000;
        return a+(num1->__forwarding->num1)+(num2->__forwarding->num2)+100;
    }
关于__forwarding这个变量:

设置在栈上的block,myBlock这个名字变量”作用域结束时,block变量也会废弃。
所以,iOS提供了将block结构体和_block变量,复制到堆上的方法。即使block的name变量结束,那么堆上的block还可以继续访问。
而此时,_block变量结构体中的_forwarding变量可以实现,无论在堆上还是在栈上。都可以正确访问_block变量。可以理解,当把_block变量复制到堆上的时候,_forwarding就指向堆里中的自己。所以无论是访问栈中自己,还是堆中的自己,最终访问都是堆中的这个值。

一个Block对_block的内存管理方式与 ARC机制完全相同。
而_main_block_desc 中的copy和dispose就是这个 __block的retain和release操作。

那什么block在时候会复制到堆呢?

  1. 掉用block的copy方法。

  2. block作为函数返回值返回时。

  3. block调用外面的_strong的id的类时,或用_block时。

  4. 方法中,用usingblock或者GCD中的API时。

这里想讲一下,在局部函数里,定义block时,打印出来还是NSConcreteGlobalBlock类型的,而且只要用了外部变量,不管是assign还是week还是strong类型的,打印出来都是NSConcreteMallocBlock类型的。所以我猜测这会不会是苹果新版的改进,为了block在访问无效的变量,直接把block拷贝到堆上,从而也拷贝一份变量。或许是我忽略了中间的某个步骤

其实到了这里,不用再描述,也知道为什么会发送死循环,又怎么解决了。当在block中用self的时候,block拷贝到堆上,首先,在栈上的这个block有一个持有者,是name这个变量。当name这个变量作用域之外,栈上这个block就release了。,那么当block拷贝到堆上的时候,block有一个持有者是self,那么block在拷贝时,它的变量一个self指针,也会拷贝,而self又指向这个block,block持有self,self持有block,两者都不会释放。要打破这个循环,需要将self置为__week,就算拷贝一个week指针,那也不影响self的引用计数。

往下看代码会发现有一个OC的属性列表和方法列表:

static struct/*_ivar_list_t*/ {
unsignedint entsize; // sizeof(struct _prop_t)
unsignedint count;
struct _ivar_t ivar_list[2];
} _OBJC_$_INSTANCE_VARIABLES_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_ivar_t),
2,
{{(unsignedlong int *)&OBJC_IVAR_$_ViewController$_str1,"_str1", "@\"NSString\"",3, 8},
{(unsignedlong int *)&OBJC_IVAR_$_ViewController$_str2,"_str2", "@\"NSString\"",3, 8}}
};

static struct/*_method_list_t*/ {
unsignedint entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[6];
} _OBJC_$_INSTANCE_METHODS_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
6,
{{(struct objc_selector *)"viewDidLoad","v16@0:8", (void *)_I_ViewController_viewDidLoad},
{(struct objc_selector *)"touchesBegan:withEvent:","v32@0:8@16@24", (void *)_I_ViewController_touchesBegan_withEvent_},
{(struct objc_selector *)"str1","@16@0:8", (void *)_I_ViewController_str1},
{(struct objc_selector *)"setStr1:","v24@0:8@16", (void *)_I_ViewController_setStr1_},
{(struct objc_selector *)"str2","@16@0:8", (void *)_I_ViewController_str2},
{(struct objc_selector *)"setStr2:","v24@0:8@16", (void *)_I_ViewController_setStr2_}}
};

这里引入一个知识点,oc中方法的调用实质是消息的发送,在Objective-C中,message与方法的真正实现是在执行阶段绑定的,而非编译阶段。编译器会将消息发送转换成对objc_msgSend方法的调用。objc_msgSend方法含两个必要参数:receiver、方法名(即:selector),如:[receivermessage]; 将被转换为:objc_msgSend(receiver,selector);objc_msgSend方法也能hold住message的参数,如:objc_msgSend(receiver,selector,para)。

当向一个对象发送消息时,objc_msgSend方法根据对象的isa指针找到对象的类,然后在类的调度表(dispatch table)中查找selector。如果无法找到selector,objc_msgSend通过指向父类的指针找到父类,并在父类的调度表(dispatch table)中查找selector,以此类推直到NSObject类。一旦查找到selector,objc_msgSend方法根据调度表的内存地址调用该实现。通过这种方式,message与方法的真正实现在执行阶段才绑定。

selector作为一个指针,本身无法直接获取到对应的函数。

将selector看作是一种方法编号,通过selector能够快速找到对应的imp指针,imp指针即是函数指针,能够指向某一个函数的实现。(implementation)

selector和imp会有一个对应的映射表,如这张图:

static struct /*_method_list_t*/ {
unsigned int entsize;  // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[6];
} _OBJC_$_INSTANCE_METHODS_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
6,
{{(struct objc_selector *)"viewDidLoad", "v16@0:8", (void *)_I_ViewController_viewDidLoad},
{(struct objc_selector *)"touchesBegan:withEvent:", "v32@0:8@16@24", (void*)_I_ViewController_touchesBegan_withEvent_},
{(struct objc_selector *)"str1", "@16@0:8", (void *)_I_ViewController_str1},
{(struct objc_selector *)"setStr1:", "v24@0:8@16", (void *)_I_ViewController_setStr1_},
{(struct objc_selector *)"str2", "@16@0:8", (void *)_I_ViewController_str2},
{(struct objc_selector *)"setStr2:", "v24@0:8@16", (void *)_I_ViewController_setStr2_}}
};

例如(struct objc_selector *)"setStr2:",即是sel指针,指向(void *)_I_ViewController_setStr2_}

我们再看下(void *)_I_ViewController_setStr2_的实现。

static void _I_ViewController_setStr2_(ViewController * self, SEL _cmd, NSString *str2) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct ViewController, _str2), (id)str2, 0, 1); }
前面说过,c函数中会包含两个隐藏的参数,在这里就能很好的体现出来。从第三个参数开始,即是我们手动传进去的参数。这就是str2的set方法的实现。

为了保证消息发送与执行的效率,系统会将全部selector和使用过的方法的内存地址缓存起来。每个类都有一个独立的缓存,缓存包含有当前类自己的selector以及继承自父类的selector。查找调度表(dispatch table)前,消息发送系统首先检查receiver对象的缓存。

缓存命中的情况下,消息发送(messaging)比直接调用方法(function call)只慢一点点点点。


这里最后补充一点,当一个msgSend执行了之后,顺着isa指针找到了对应的方法就会发送消息(一层一层找,如果子类找不到就会在父类找)。但是如果找不到对应方法的实现,是否就会crash,报unrecognized selector sent to instance 0x87?这里有一个消息转发的机制。

消息转发分为两大阶段。
第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知选择子”,成为“动态方法解析”。
第二阶段涉及“完整消息转发”。如果第一阶段执行完,还是没有找到相应的选择子对应的方法,此时运行期系统会请求接收者以其他手段来处理与消息有关的方法调用。这里又分为两小步:
首先,请接收者看看有没有其他对象能够处理这条消息。若有,则运行期系统会把消息转给那个对象,这个过程称为“备援接收者”。
其次,如果没有,则进入第二小步“完整的消息转发”,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。


参考资料:http://www.cocoachina.com/ios/20160307/15441.html

http://www.cocoachina.com/ios/20160830/17424.html

如果有觉得哪些地方笔者理解得有偏差,请联系我~




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值