iOS进阶面试题(一)

后10题面试题见iOS进阶面试题(二)

面试题.png

1.详情描述一下UIView 与CALayer 的关系,drawRect 一定会影响性能吗?UIDynamics 与 UIKit Animation 的最本质的区别是什么?

解答:

Q1. UIView 与CALayer 的关系?
  1. UIView可以响应事件,CALayer不可以。
    • UIKit使用UIResponder作为响应对象,来响应系统传递过来的事件并进行处理。在 UIResponder中定义了处理各种事件和事件传递的接口。
      UIApplication、UIViewController、UIView、和所有从UIView派生出来的UIKit类(包括UIWindow)都直接或间接地继承自UIResponder类。
      CALayer直接继承 NSObject,并没有相应的处理事件的接口。
  2. UIView 是 CALayer的delegate
    • 在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
  3. UIView主要处理事件,CALayer负责绘制
  4. 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
  5. UIView 和 CALayer 不是线程安全的,并且只能在主线程创建、访问和销毁。

下面是拓展知识可以略过

  • 屏幕显示图像原理


    iOS进阶面试题(一)_第1张图片
    屏幕显示图像原理.png

通常来说,计算机系统中 CPU、GPU、显示器是以上面这种方式协同工作的。CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

  • 卡顿产生的原因和解决方案


    iOS进阶面试题(一)_第2张图片
    卡顿产生的原因.png

    在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
    从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

  • GPU 资源消耗原因和解决方案
    相对于 CPU 来说,GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,然后输出到屏幕上。通常你所能看到的内容,主要也就是纹理(图片)和形状(三角模拟的矢量图形)两类。

Q2. drawRect 一定会影响性能吗?

Core Graphics绘制 ,如果对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。

Q3. UIDynamics 与 UIKit Animation 的最本质的区别是什么?

动力系统的引入,并不是替代CoreAnimation,而是对模拟现实世界物体运动的补充,比如,碰撞,重力,悬挂等等。所以说,UIKit动力系统的引入,大大简化了一些交互动画(不需要自己实现一些模拟现实世界物理动力系统的动画),丰富了UI设计。

1. 如何用 UIImageView 显示超大分辨率的图?如果要支持缩放呢?

解答:

  • Q1 一般的显示超大图:
    肯定是需要预先处理的也就是压缩图片.

压缩:压缩图片我们希望可以保证压缩的速度够快及内存消耗的尽可能小。在此感谢github上的OTLargeImageReader的作者,压缩过程中内存控制和速度都很好。

//先从内存中查找,查找不到再解码,避免重复解码
UIImage *cacheImage = [self.photoBrowser cacheImageWithPhoto:_photo];
if (cacheImage == nil) {
    //不存在,解码
    [self.photoBrowser showHUDWithSuperBigPhoto];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        CGSize compressSize = CGSizeMake(XXPhotoCompressPixelMax, XXPhotoCompressPixelMax);
        if (image.size.width > image.size.height) {
            compressSize = CGSizeMake(XXPhotoCompressPixelMax, XXPhotoCompressPixelMax*image.size.height/image.size.width);
        }
        else {
            compressSize = CGSizeMake(XXPhotoCompressPixelMax*image.size.width/image.size.height, XXPhotoCompressPixelMax);
        }
        UIImage *compressedImage = [image imageByScalingProportionallyToSize:compressSize];
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.photoBrowser cacheImageWithPhoto:_photo image:compressedImage];
            self.showImageView.image = compressedImage;
            [self.photoBrowser hideHUDWithSuperBigPhoto];
            [self resetSize];
        });
    });
}
else {
    //直接使用
    self.showImageView.image = cacheImage;
}
  • Q2 支持缩放呢:
    解决方案是,裁剪只显示屏幕能看见的部分,这样大图缩放起来也不会模糊了。
- (void)didCutImage {
    if (_orImage) {
        if (self.scrollView.contentSize.width >= kScreenWidth &&
            self.scrollView.contentSize.height >= kScreenHeight) {
            CGFloat multipleF = _orImage.size.width/self.scrollView.contentSize.width;
            CGFloat width = kScreenWidth*multipleF;
            CGFloat height = kScreenHeight *multipleF;
            //如果剪切的尺寸过大,不处理
            if (width > XXPhotoPixelMax ||
                height > XXPhotoPixelMax) {
                return;
            }
            //如果剪切的尺寸过大,不处理
            //裁剪展示视图
            if (_bigCupImageView) {
                _bigCupImageView.frame = CGRectMake(self.scrollView.contentOffset.x, self.scrollView.contentOffset.y, kScreenWidth, kScreenHeight);
            }
            else {
                [self.scrollView addSubview:self.bigCupImageView];
            }
            //裁剪展示视图
            CGImageRef cgRef = _orImage.CGImage;
            CGImageRef imageRef = CGImageCreateWithImageInRect(cgRef, CGRectMake(self.scrollView.contentOffset.x *multipleF   ,self.scrollView.contentOffset.y *multipleF, width, height));
            UIImage *thumbScale = [UIImage imageWithCGImage:imageRef];
            CGImageRelease(imageRef);
            self.bigCupImageView.image = thumbScale;
        }
    }
}

在这个过程中,仍需要注意的是,何时展示与隐藏剪切出来的图片。
覆盖图片的添加与移除:
添加:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [NSObject cancelPreviousPerformRequestsWithTarget:self];
    [self performSelector:@selector(didCutImage) withObject:nil afterDelay:.5];
}

移除:

- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view {
    if (_bigCupImageView) {
        [_bigCupImageView removeFromSuperview];
        _bigCupImageView = nil;
    }
}

3.了解 fishhook 吗?说说为什么 fishhook 不能修改非动态连接库中的符号?

解答:
1.Q1:简单来说Fishhook就是hook函数的一种工具,当然它hook的原理和我们熟知的Method Swizzle 方式是不一样的,它是Facebook提供的一个动态修改链接mach-O文件的工具。

  • Method Swizzle 是利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。
  • Fishhook 是利用MachO文件加载原理,通过修改懒加载和非懒加载两个表的指针达到C函数HOOK的目的。
  1. Q2:
  • 为什么fishhook可以交换C函数
    fishhook 是通过改变dyld加载动态库所用的指针指向的函数地址,来实现的函数交换 ,所以C函数通过dyld动态加载动态库也展现出C函数动态的一面。
  • 为什么我们自己写的C函数fishhook交换不了
    我们自己写的C函数不在系统动态库缓存区,而是存在我们自己的MachO文件当中,不经过dyld加载,直接编译成汇编语言,在MachO中的TEXT段中。所以fishhook的原理不能交换我们自己写的C函数。

4.C++ 调用虚方法与 Objective-C 发消息有什么区别?

  1. Objective-C提供了运行期动态绑定机制,而C++是编译静态绑定,并且通过嵌入类(多重继承)和虚函数(虚表)来模拟实现。
  2. Objective-C在语言层次上支持动态消息转发,其函数调用语法为[objece message],而且C++为object->function()。
  3. 两者的语义也不同,在Objective-C里是说发送消息给 一个对象,至于这个对象能不能处理消息以及是处理还是转发都不crash,而在C++里是说对象进行了某个操作,如果对象没有这个操作的话,要么编译会报错(静态绑定),要么程序会crash掉的(动态绑定)。
    4.C++生成的是机器代码,因此C++需要虚函数来声明方法,然后编译器针对性的编译实现函数的虚基表(实质就是一个代理,查找函数的实际地址)来实现多态,实质如此实现也会带来不小的性能损耗。而OC更加动态一些,当然性能损耗也更厉害一些,不过这种性能损耗,带来的是更灵活以及更简单的实现,提高了开发效率。

5.了解 placement new 吗?Objective-C 中如何实现这个功能?

解答:
Q1:在C++中因为可以对运算符进行重载,所以只需对operator new()进行placement重载就能实现在事先分配好的内存空间上调用构造函数来动态创建对象,通过不太常用的对析构函数的显式调用来销毁对象。在这种情况下不涉及内存空间的分配和释放,从而不会导致动态的存储管理,不会对系统的长期稳定运行带来影响。
Q2:Objective-C 中如何实现这个功能?
Objective-C中所有的对象都是指针,所有的对象都继承NSObject,修改isa 的指向即可,感觉没有必要实现C++的这个功能,如果想实现的是肯定是可以的应为都是源自C的嘛。

6.如何在 ARC 环境下用 C++ 标准库容器来管理 Objective-C 对象?

解答:参考
C++ 开发者使用 Objective-C 和 ARC 的重要提示

// C++
class A {
public:

   NSObject* s;
   A();
};     

A :: A()
 {
 s = 0; // 可能会奔溃,这是常发生在发布模式下!
 }

让我们来看看这里发生了什么. s = 0 这一行将 0 赋值给了一个变量,而因此不管这个变量之前取值是什么,首先都会被释放掉,所以编译器在赋值之前执行了一次 [s release] . 如果 s 之前已经是 0 了,假设是在调试构建的话,不会发生任何不好的事情; 如果 s 是一个nil的话,使用[s release] 是再好也不过的. 然而,在发布模式下, s可能是一个野指针,所以它可能在被“初始化”为0之前包含任何值(这很危险是不是?).

在C++中,这并不是一个问题,因为它里面是不存在ARC的. 而在Objective-C中编译器并没有办法了解这是一次"赋值" 还是一次 "初始化" (如果是后者,它就不会发送发布消息).

下面是正确的方式:

// C++
class A {
public:
   NSObject* s;
   A();
};     

A :: A() :s(0) // 现在编译器就知道确定 it's 是一次初始化了, 一次就不存在 [s release]
 {
 }

现在编译器就不会去尝试调用一个 [s release] 因为它知道它是这个对象的第一次初始化. 请小心!

7. id、self、super 它们从语法上有什么区别?

  • id 类型是iOS中一种特殊的动态数据类型,其存在价值:
    id是一种通用的对象类型,她可以用类存储属于任何类的对象,可以理解为万能指针
    NSObject 和id都可以指向任何对象
    NSObject对象会惊醒编译时检查(需要强制类型转换)
    id不需要强制类型转换,id可以直接使用
    编译器看到id以后,认为是动态类型,不在检查类型

  • self 调用自己方法,super 调用父类方法

  • self是类隐藏参数,super是预编译指令
    【self class】和【super class】输出是一样的

objc_msgSend(id self,SEL _cmd)

这个函数,在当前类结构中找到方法并且调用

  • [super message] 会转化为
id objc_msgSendSuper(struct __rw_objc_super *super, SEL op, …) 

对比发现这里除了函数名加了super以外,第一个参数由self变成了一个结构体,下面让我们来解开这个结构体的真面目

struct __rw_objc_super { 
    struct objc_object *object; //代表当前类的对象
    struct objc_object *superClass; 
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};

这个结构体中有两个参数:object的对象和一个superClass的结构体指针,这里的object相当于上面的self
在执行[super message]时,会做下面的事

  1. 编译器会先构造一个__rw_objc_super的结构体
  2. 然后去superClass的方法列表中找方法
  3. 找到之后由object调用。
    所以当你用[self Class]和[super Class]打印类的时候,打印的都是同一个类,因为他们只是查找方法的位置不同,但是调用方法的类/对象是一样的.

8.isa 是什么?是指向 Class 对象本身的指针吗?

  • 一个对象(Object)的isa指向了这个对象的类(Class),而这个对象的类(Class)的isa指向了metaclass。


    iOS进阶面试题(一)_第3张图片
    image.png
  • 示例:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [[Person alloc] init];
        //输出1
        NSLog(@"%d", [p class] == object_getClass(p));
        //输出0
        NSLog(@"%d", class_isMetaClass(object_getClass(p)));
        //输出1
        NSLog(@"%d", class_isMetaClass(object_getClass([Person class])));
        //输出0
        NSLog(@"%d", object_getClass(p) == object_getClass([Person class]));
    }
    return 0;
}

通过代码可以看出,一个实例对象通过class方法获取的Class就是它的isa指针指向的类对象,而类对象不是元类,类对象的isa指针指向的对象是元类。

9.block 修改捕获变量除了用 __block 还可以怎么做?有哪些局限性?

解答:

1. 自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量。
    1. 静态全局变量,全局变量由于作用域的原因,于是可以直接在Block里面被改变。他们也都存储在全局区。
iOS进阶面试题(一)_第4张图片
image.png
    1. 静态变量传递给Block是内存地址值,所以能在Block里面直接改变值。苹果要求我们在自动变量前加入 __block关键字(__block storage-class-specifier存储域类说明符),就可以在Block里面改变外部自动变量的值了。

总结: 在Block中改变变量值有2种方式, 一是传递内存地址指针到Block中, 二是改变存储区方式(__block)。

  • 实例:
#import 
 
int main(int argc, const char * argv[]) {
 
  NSMutableString * str = [[NSMutableString alloc]initWithString:@"Hello,"];
 
        void (^myBlock)(void) = ^{
            [str appendString:@"World!"];
            NSLog(@"Block中 str = %@",str);
        };
 
    NSLog(@"Block外 str = %@",str);
 
    myBlock();
 
    return 0;
}
  • 输出
Block 外  str = Hello,
Block 中  str = Hello,World!

源码.cpp

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableString *str;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_str, int flags=0) : str(_str) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableString *str = __cself->str; // bound by copy
 
            ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_1);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_2,str);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}
 
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}
 
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
 
int main(int argc, const char * argv[]) {
    NSMutableString * str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_0);
 
        void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344));
 
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_3,str);
 
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
 
    return 0;
}

在__main_block_func_0里面可以看到传递的是指针。所以成功改变了变量的值。

2. OC中,一般Block就分为以下3种,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。

2.1.从捕获外部变量的角度上来看

  • _NSConcreteStackBlock:
    只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。
    StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。

  • _NSConcreteMallocBlock:
    有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制

  • _NSConcreteGlobalBlock:
    没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。

2.2 .从持有对象的角度上来看:

  • _NSConcreteStackBlock是不持有对象的。
//以下是在MRC下执行的
    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);
 
    void (^myBlock)(void) = ^{
        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };
 
    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);
 
    myBlock();

输出:

1.Block外 obj = 1
2.Block外 obj = 1
Block中 obj = 1
  • _NSConcreteMallocBlock是持有对象的。
//以下是在MRC下执行的
    NSObject * obj = [[NSObject alloc]init];
    NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount);
 
    void (^myBlock)(void) = [^{
        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    }copy];
 
    NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount);
 
    myBlock();
 
    [myBlock release];
 
    NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount);

输出

1.Block外 obj = 1
2.Block外 obj = 2
Block中 obj = 2
3.Block外 obj = 1
  • _NSConcreteGlobalBlock也不持有对象
//以下是在MRC下执行的
    void (^myBlock)(void) = ^{
 
        NSObject * obj = [[NSObject alloc]init];
        NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount);
    };
 
    myBlock();

输出

Block 中 obj = 1

由于_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。比如当Block作为函数返回值的时候,肯定会copy到堆上。

1.手动调用copy
2.Block是函数的返回值
3.Block被强引用,Block被赋值给__strong或者id类型
4.调用系统API入参中含有usingBlcok的方法

以上4种情况,系统都会默认调用copy方法把Block赋复制

但是当Block为函数参数的时候,就需要我们手动的copy一份到堆上了。这里除去系统的API我们不需要管,比如GCD等方法中本身带usingBlock的方法,其他我们自定义的方法传递Block为参数的时候都需要手动copy一份到堆上。

copy函数把Block从栈上拷贝到堆上,dispose函数是把堆上的函数在废弃的时候销毁掉。

3..Block中__block实现原理

1.普通非对象的变量

#import 
 
int main(int argc, const char * argv[]) {
 
    __block int i = 0;
 
    void (^myBlock)(void) = ^{
        i ++;
        NSLog(@"%d",i);
    };
 
    myBlock();
 
    return 0;
}

把上述代码用clang转换成源码。

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int i;
};
 
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref
 
        (i->__forwarding->i) ++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
 
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
 
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
 
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
 
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
 
    return 0;
}

从源码我们能发现,带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,这个结构体有5个成员变量。第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。

__forwarding指针这里的作用就是针对堆的Block,把原来__forwarding指针指向自己,换成指向_NSConcreteMallocBlock上复制之后的__block自己。然后堆上的变量的__forwarding再指向自己。这样不管__block怎么复制到堆上,还是在栈上,都可以通过(i->__forwarding->i)来访问到变量值。


iOS进阶面试题(一)_第5张图片
image.png

ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上。

MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上,block也只是__NSStackBlock,这个时候__forwarding指针就只指向自己了。


iOS进阶面试题(一)_第6张图片
image.png

2.对象的变量

//以下代码是在ARC下执行的
#import 
 
int main(int argc, const char * argv[]) {
 
    __block id block_obj = [[NSObject alloc]init];
    id obj = [[NSObject alloc]init];
 
    NSLog(@"block_obj = [%@ , %p] , obj = [%@ , %p]",block_obj , &block_obj , obj , &obj);
 
    void (^myBlock)(void) = ^{
        NSLog(@"***Block中****block_obj = [%@ , %p] , obj = [%@ , %p]",block_obj , &block_obj , obj , &obj);
    };
 
    myBlock();
 
    return 0;
}

输出:

block_obj = [ , 0x7fff5fbff7e8] , obj = [ , 0x7fff5fbff7b8]
Block****中********block_obj = [ , 0x100f000a8] , obj = [ , 0x100f00070]

我们把上面的代码转换成源码研究一下:

struct __Block_byref_block_obj_0 {
  void *__isa;
__Block_byref_block_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 id block_obj;
};
 
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  id obj;
  __Block_byref_block_obj_0 *block_obj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _obj, __Block_byref_block_obj_0 *_block_obj, int flags=0) : obj(_obj), block_obj(_block_obj->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_block_obj_0 *block_obj = __cself->block_obj; // bound by ref
  id obj = __cself->obj; // bound by copy
 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_e64910_mi_1,(block_obj->__forwarding->block_obj) , &(block_obj->__forwarding->block_obj) , obj , &obj);
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->block_obj, (void*)src->block_obj, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
 
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->block_obj, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}
 
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
 
 
int main(int argc, const char * argv[]) {
 
    __attribute__((__blocks__(byref))) __Block_byref_block_obj_0 block_obj = {(void*)0,(__Block_byref_block_obj_0 *)&block_obj, 33554432, sizeof(__Block_byref_block_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
 
    id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_e64910_mi_0,(block_obj.__forwarding->block_obj) , &(block_obj.__forwarding->block_obj) , obj , &obj);
 
    void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, obj, (__Block_byref_block_obj_0 *)&block_obj, 570425344));
 
    ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
 
    return 0;
}

总结:

在MRC环境下,__block根本不会对指针所指向的对象执行copy操作,而只是把指针进行的复制。
而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内不安全的引用外部对象,所以才会产生循环引用的问题!

5种变量,自动变量,函数参数 ,静态变量,静态全局变量,全局变量,如果严格的来说,捕获是必须在Block结构体__main_block_impl_0里面有成员变量的话,Block能捕获的变量就只有带有自动变量和静态变量了。捕获进Block的对象会被Block持有。

对于非对象的变量来说,
自动变量的值,被copy进了Block,不带__block的自动变量只能在里面被访问,并不能改变值。


iOS进阶面试题(一)_第7张图片
image.png

带__block的自动变量 和 静态变量 就是直接地址访问。所以在Block里面可以直接改变变量的值。


iOS进阶面试题(一)_第8张图片
image.png

而剩下的静态全局变量,全局变量,函数参数,也是可以在直接在Block中改变变量值的,但是他们并没有变成Block结构体__main_block_impl_0的成员变量,因为他们的作用域大,所以可以直接更改他们的值。

值得注意的是,静态全局变量,全局变量,函数参数他们并不会被Block持有,也就是说不会增加retainCount值。

对于对象来说,

在MRC环境下,__block根本不会对指针所指向的对象执行copy操作,而只是把指针进行的复制。
而在ARC环境下,对于声明为__block的外部对象,在block内部会进行retain,以至于在block环境内能安全的引用外部对象。

10. NSDictionary 与 NSHashTable 有什么区别,它们的使用场景是怎样的?

我们使用集合(NSArray,NSMutableArray,NSDictionary,NSMutableDictionary,NSSet,NSMutableSet)存储对象时会对其强引用,有时候我们不想这样子,怎么办呢?
那就使用NSHashTable这个集合吧,它的使用方法与NSSet完全相似,不同的是,它的一种初始化方式是weakObjectsHashTable,专门用来存储弱引用对象,不会持有它,那个对象的所有人消失了,这个对象也会从这个集合中消失!
常用用法:

- (BOOL)containsObject:(id)anObject
Returns a Boolean value that indicates whether the hash table contains a given object.
返回一个bool值,用来指示这个hash表中是否包括了你给与的对象.
- (void)addObject:(id)object
Adds a given object to the hash table.
将一个对象添加进hash表中.
- (void)removeObject:(id)object
Removes a given object from the hash table.
从hash表中移除你给定的对象.
+ (id)weakObjectsHashTable
Returns a new hash table for storing weak references to its contents.
返回一个hash表用来存储弱引用对象.

你可能感兴趣的:(iOS进阶面试题(一))