InterView一个靠谱的iOS开发(三)

1. 工作中遇到比较难的问题是怎么解决的?

重构应该是大家会遇到的比较通用的问题,甚至会纳入到KPI考核。 首先要梳理流程,比如重构的目的制定重构工作流重构过程~验证结果。

  • 重构的目的:重构的原因无非就是代码结构混乱、逻辑混乱以及在新需求面前无法拓展。所以要重构,给代码分层,增强拓展性,结合公司实际业务来设定特性。
  • 重构的工作流:梳理设计重构。梳理这部分很重要,因为即将要动原来的代码,所以最好是把现有的代码先整体梳理一遍,在哪些地方被调用了,调用了哪些地方。最好也进行功能梳理。最后把梳理的成果文档化。文档化是我比较推崇的,因为一方面别的同事可以直接查看文档而不需要时常打断你的工作;一方面以后自己也可以进行回顾。不要太高估自己的对代码的记忆,过了那么一、两个月,即使是自己写的代码也不可能完全记得。
  • 重构过程:把代码分层,抽离组件,引入调度中心模式作为总控,方便后续模块的重构。等等。要有冗余措施,业务上做到动态可切换回退。
  • 验证结果:开发自测和测试人员配合完成。

2. Block实现原理

2.1 Block存储域:栈Block、堆Block、全局Block的三种分类。

  • 为全局Block的情况: 声明全局变量的地方有Block语法,Block语法的表达式中不捕获自动变量时。没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。
  • 为栈Block的情况:普通声明局部Block局部变量。只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。
  • 为堆Block的情况:调用copy函数,将栈Block复制到堆Block。有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制
  • Block从栈上自动复制到堆:
    • 在ARC大多数情况下,Block栈变量会被编译器自动地进行判断,生成将Block从栈上复制到堆上的代码。由于其被复制到了堆上,可以常驻内存,因此仍然在存活的生命周期内。
    • Block作为函数返回值时;Block作为GCD API的方法参数时;Block作为系统方法名含有usingBlock的方法参数时;Block被强引用,如Block被赋值给__strong或者id类型;
  • 向方法或函数的参数中传递Block时,最好手动将Block从栈复制到堆上编译器不能进行判断,又不能无脑使用copy【耗性能】)

2.3 Block是怎样捕获的外部变量

  • 在 Block 中使用外部的局部变量时,会自动捕获该变量并且成为 Block 结构体的成员变量,以便在 Block 内部访问该变量.
#import 

int global_i = 1;

static int static_global_j = 2;

int main(int argc, const char * argv[]) {

   static int static_k = 3;
   int val = 4;

   void (^myBlock)(void) = ^{
       global_i ++;
       static_global_j ++;
       static_k ++;
       //var ++;
       NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
   };

   global_i ++;
   static_global_j ++;
   static_k ++;
   val ++;
   NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);

   myBlock();

   return 0;
}
Block 外  global_i = 2,static_global_j = 3,static_k = 4,val = 5
Block 中  global_i = 3,static_global_j = 4,static_k = 5,val = 4
  • 全局变量global_i和静态全局变量static_global_j的值增加,以及它们被Block捕获进去,这一点很好理解,因为是全局的,作用域很广,所以Block捕获了它们进去之后,在Block里面进行++操作,Block结束之后,它们的值依旧可以得以保存下来。

  • 全局变量 和 静态全局变量 因其作用域内都可以直接访问。静态变量成为成员变量,但是从构造函数传入的是一个内存地址,然后通过地址访问。局部变量 成为成员变量,从构造函数直接传入变量的值并赋值给成员变量,然后通过成员变量访问.

  • 通过clang对Block转换源码可以看到__main_block_impl_0结构体可以观察出,在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。

2.2 __bolck实现原理

  • 通过clang出的源码我们能发现,带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,这个结构体有5个成员变量。第一个是isa指针,第二个是指向自身类型的__forwarding指针,第三个是一个标记flag,第四个是它的大小,第五个是变量值,名字和变量名同名。
  • 一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上。
  • ARC环境下,Block捕获外部对象变量,是都会copy一份的,地址都不同。只不过带有__block修饰符的变量会被捕获到Block内部持有。对于非对象的变量来说,自动变量的值,被copy进了Block,不带__block的自动变量只能在里面被访问,并不能改变值。带__block的自动变量 和 静态变量 就是直接地址访问。所以在Block里面可以直接改变变量的值。

3.autorealespool 是怎样工作的?

  • 被autoreleasepool包裹的代码编译后会变成
void * atautoreleasepoolobj = objc_autoreleasePoolPush();
// 中间代码
objc_autoreleasePoolPop(atautoreleasepoolobj);

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}
  • push 和 pop函数其实是调用的AutoreleasePoolPage的push 和 pop
class AutoreleasePoolPage {
   magic_t const magic; //用于对当前 AutoreleasePoolPage 完整性的校验
   id *next;  // 下一个可插入对象的可用地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:
   pthread_t const thread; // 当前的线程
   AutoreleasePoolPage * const parent; // 父指针
   AutoreleasePoolPage *child; // 子指针
   uint32_t const depth;
   uint32_t hiwat;
};

其实是一个双向链表,每个节点都是一个AutoreleasePoolPage,每加入一个对象next指针就偏移一次。

  • POOL_BOUNDARY(哨兵对象)
    也有人称作 POOL_SENTINEL
    POOL_BOUNDARY 只是 nil 的别名
    在每个自动释放池初始化调用 objc_autoreleasePoolPush 的时候,都会把一个 POOL_BOUNDARY push 到自动释放池的栈顶,并且返回这个 POOL_BOUNDARY 哨兵对象。可以理解为一种token。
  • push的过程会经过一系列的判断,比如当前page是否满了等,遍历双向链表等,将对象放入AutoreleasePoolPage的栈中。
  • pop的过程中使用 pageForPointer 获取当前 token(哨兵对象), 所在的 AutoreleasePoolPage;调用 releaseUntil 方法释放栈中的对象,直到 stop,调用 child 的 kill 方法。kill的过程中也会经过一系列的判断,当前page是否为空等,释放当前和所有的child。

4. 堆,栈,队列

4.1 堆

  • 堆是一种经过排序的树形数据结构,每个节点都有一个值,通常我们所说的堆的数据结构是指二叉树。所以堆在数据结构中通常可以被看做是一棵树的数组对象。而且堆需要满足一下两个性质:
    • 堆中某个节点的值总是不大于或不小于其父节点的值;
    • 堆总是一棵完全二叉树。
  • 堆分为两种情况,有最大堆和最小堆。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆,在一个摆放好元素的最小堆中,父结点中的元素一定比子结点的元素要小,但对于左右结点的大小则没有规定谁大谁小。
  • 堆常用来实现优先队列,堆的存取是随意的。

4.2 栈

  • 栈是限定仅在表尾进行插入和删除操作的线性表。我们把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何数据元素的栈称为空栈
  • 栈是一种具有后进先出的数据结构,又称为后进先出的线性表。栈的应用—递归

4.3 队列

  • 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。允许插入的一端称为队尾,允许删除的一端称为队头。
  • 队列是一种先进先出的数据结构,又称为先进先出的线性表。

4.4 如何用两个栈模拟一个队列

  • 思路:准备2个栈:inStack,outStack;入队时,push到inStack中;出队时,如果outStack为空,将inStack所有元素逐一弹出,push到outStack,outStack弹出栈顶元素;如果outStack不为空,outStack弹出栈顶元素。
#import "YZQueue.h"
#import "YZStack.h"

@interface YZQueue ()

@property (nonatomic, strong) YZStack *inputStack;
@property (nonatomic, strong) YZStack *outStack;

@end

@implementation YZQueue

//获取元素的数量
- (NSInteger)size {
    return self.inputStack.size + self.outStack.size;
}

//队列是否为空
- (BOOL)isEmpty {
    return self.inputStack.isEmpty && self.outStack.isEmpty;
}
//入队
- (void)enterQueue:(id)value {
    [self.inputStack push:value];
}
//出队
- (id)deQueue {
    if (self.outStack.isEmpty) {
        while (!self.inputStack.isEmpty) {
            [self.outStack push:self.inputStack.pop];
        }
    }
    return self.outStack.pop;
}
//获取对象的头元素
- (id)front {
    if (self.outStack.isEmpty) {
        while (!self.inputStack.isEmpty) {
            [self.outStack push:self.inputStack.pop];
        }
    }
    return self.outStack.top;
}

#pragma mark - 懒加载
- (YZStack *)inputStack {
    if (!_inputStack) {
        _inputStack = [[YZStack alloc] init];
    }
    return _inputStack;
}

- (YZStack *)outStack {
    if (!_outStack) {
        _outStack = [[YZStack alloc] init];
    }
    return _outStack;
}

@end

5.动态库和静态库的区别

  • 库:是资源文件和代码编译的一个集合
  • 静态库: 静态库是在编译时,完整的拷贝至可执行文件中,被多次使用就有多次冗余拷贝;.a和.framework。
  • 动态库: 程序运行时由系统动态加载到内存,而不是复制,供程序调用。系统只加载一次,多个程序共用,节省内存。因此,编译内容更小,而且因为动态库是需要时才被引用,所以更快。.dylib和.framework。
  • framework为什么可能是动态库,也可能是静态库?系统的.framework是动态库, 我们自己建立的.framework是静态库。
  • 静态和动态的名字,区分了编译后的代码是以何种方式链接到目标程序中的。

6.结构体的内存对齐

基本数据类型的占用内存(全部以64位为例,32位没意义)
InterView一个靠谱的iOS开发(三)_第1张图片
image
struct LGStruct1 {
    double a;  
    char b;     
    int c;      
    short d;    
}struct1;

struct LGStruct2 {
    double a;   
    int b;      
    char c;     
    short d;    
}struct2;

struct LGStruct3 {
    double a; 
    int b;     
    char c;     
    short d;    
    struct LGStruct1 e;
}struct3;

NSLog(@"%lu-%lu-%lu",sizeof(struct1),sizeof(struct2),sizeof(struct3));

输出结果为
  • 数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第⼀个数据成员放在offset为0的地⽅,以后每个数据成员存储的起始位置要从该成员⼤⼩或者成员的⼦成员⼤⼩(只要该成员有⼦成员,⽐如说是数组,结构体等)的整数倍开始(⽐如int为4字节,则要从4的整数倍地址开始存储。
  • 结构体作为成员:如果⼀个结构⾥有某些结构体成员,则结构体成员要从其内部最⼤元素⼤⼩的整数倍地址开始存储。(struct a⾥存有struct b,b⾥有char、int 、double等元素,那b应该从8的整数倍开始存储。)
  • 收尾⼯作:结构体的总⼤⼩,也就是sizeof的结果,必须是其内部最⼤成员的整数倍,不⾜的要补⻬。

案例解析 结构体LGStruct1,通过内存对齐规则计算过程如下:

  • 变量a:**double ** 占8个字节,从0位置开始,则 0-7 存储 a
  • 变量b:**char **占1个字节,从8位置开始,此时8是1的整数倍,则 8 存储 b
  • 变量c:int 占4个字节,从9位置开始,但是此时9不是4的整数倍,因此需要往后继续寻找,找到最接近的能整除4的12位置,则 12-15 存储 c
  • 变量d:**short **占2个字节,从16位置开始,此时16是2的整数倍,则16-17 存储 d
  • 收尾:LGStruct1需要的内存大小为18字节,而LGStruct1中最⼤成员变量字节数是8字节,内存大小18字节不是内部最⼤成员的整数倍,所以必须向上补齐,补齐后的最终大小为24字节

交流

最后推荐个我的iOS交流群:789143298
'有一个共同的圈子很重要,结识人脉!里面都是iOS开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

  • ——点击加入:iOS开发交流群
    以下资料在群文件可自行下载

    InterView一个靠谱的iOS开发(三)_第2张图片

作者:闫雪同学
链接:https://juejin.cn/post/6893342479177908237

你可能感兴趣的:(InterView一个靠谱的iOS开发(三))