总结2018/07/16–2018/07/17社招面试(雪球、58金融、四达时代、洋钱罐)的经历总结,对知识进行查缺补漏
记录于此:任重道远,仍需努力,披荆斩棘,砥砺而行
开发语言(Objective-C、Swift)相关
- Block的类型
- 闭包的书写方式
- block的数据结构
struct Block_descriptor { unsigned long int reserved; unsigned long int size; void (*copy)(void *dst, void *src);//对于上下文捕获变量的copy和释放 void (*dispose)(void *); }; struct Block_layout { void *isa; int flags; int reserved; void (*invoke)(void *, ...);//指向调用函数的指针 struct Block_descriptor *descriptor; };
- isa指向class对象;FuncPtr指向调用函数的指针;拥有isa指针,说明block也是一个对象
- block就是一个里面存储了指向函数体中包含定义block时的代码块的函数指针,以及block外部上下文变量等信息的结构体
- block类型分为:_NSConcreteGlobalBlock(全局)、_NSConcreteStackBlock(栈)、_NSConcreteMallocBlock(堆)
void (^globalBlock)() = ^{ }; int main(int argc, const char * argv[]) { @autoreleasepool { void (^stackBlock1)() = ^{ }; } return 0; }
- globalBlock的isa指向了_NSConcreteGlobalBlock,即在全局区域创建,编译时具体的代码就已经确定在上图中的代码段中了,block变量存储在全局数据存储区;stackBlock的isa指向了_NSConcreteStackBlock,即在栈区创建。即全局的block存储在全局数据存储区域;栈block在栈区创建
- 堆中的block无法直接创建,其需要由_NSConcreteStackBlock类型的block拷贝而来(也就是说block需要执行copy之后才能存放到堆中)。block的拷贝最终都会调用_Block_copy_internal函数
static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; ... aBlock = (struct Block_layout *)arg; ... // Its a stack block. Make a copy. if (!isGC) { // 申请block的堆内存 struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void *)0; // 拷贝栈中block到刚申请的堆内存中 memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // reset refcount result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 1; // 改变isa指向_NSConcreteMallocBlock,即堆block类型 result->isa = _NSConcreteMallocBlock; if (result->flags & BLOCK_HAS_COPY_DISPOSE) { //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock); (*aBlock->descriptor->copy)(result, aBlock); // do fixup } return result; } else { ... } }
- 不管是用什么形式访问实例变量,最终都会转换成self+变量内存偏移的形式
- ARC会自动帮strong类型且捕获外部变量的block进行copy,所以在定义block类型的属性时也可以使用strong,不一定使用copy;当block类型为strong,但是创建时没有捕获外部变量,block最终会变成__NSGlobalBlock__类型(这里可能因为block中的代码没有捕获外部变量,所以不需要在栈中开辟变量,也就是说,在编译时,这个block的所有内容已经在代码段中生成了,所以就把block的类型转换为全局类型)
- block对于使用__block修饰的自动变量,会将其包装为对象,使其移动到堆上;对于单纯的局部自动变量只是值传递,只是保存了捕获时的取值,故不会受到外界变量的影响,也不能影响外界的变量
- Runtime
- OC语言在编译期都会被编译为C语言的Runtime代码,二进制执行过程中执行的都是C语言代码。而OC的类本质上都是结构体,在编译时都会以结构体的形式被编译到二进制中。Runtime是一套由C、C++、汇编实现的API,所有的方法调用都叫做发送消息
- 每个对象都是一个objc_object的结构体,在结构体中有一个isa指针,该指针指向自己所属的类,由Runtime负责创建对象
- IMP 在Runtime中IMP本质上就是一个函数指针,其定义如下。在IMP中有两个默认的参数id和SEL,id也就是方法中的self,这和objc_msgSend()函数传递的参数一样
- Method 用来表示方法,其包含SEL和IMP;在Xcode进行编译的时候,只会将Xcode的Compile Sources中.m声明的方法编译到Method List,而.h文件中声明的方法对Method List没有影响
- Property 在Runtime中定义了属性的结构体,用来表示对象中定义的属性;可以通过class_copyPropertyList或者class_copyIvarList获取对象的公共属性列表,甚至是私有属性列表。并且可以通过KVC或者Runtime(class_getInstanceVariable/object_setIvar/object_getIvar)访问和修改私有属性
- OC中不存在真正的私有属性和私有方法
- 内存布局
- 创建实例对象时,会根据其对应的Class分配内存,内存构成是ivars+isa_t。并且实例变量不只包含当前Class的ivars,也会包含其继承链中的ivars。ivars的内存布局在编译时就已经决定,运行时需要根据ivars内存布局创建对象,**所以Runtime不能动态修改ivars,会破坏已有内存布局。**
- 创建实例对象时,会根据其对应的Class分配内存,内存构成是ivars+isa_t。并且实例变量不只包含当前Class的ivars,也会包含其继承链中的ivars。ivars的内存布局在编译时就已经决定,运行时需要根据ivars内存布局创建对象,**所以Runtime不能动态修改ivars,会破坏已有内存布局。**
- ivar的读写(访问)
- 实例变量的isa_t指针会指向其所属的类,对象中并不会包含method、protocol、property、ivar等信息,这些信息在编译时都保存在只读结构体class_ro_t中。在class_ro_t中ivars是const只读的,在image load时copy到class_rw_t中时,是不会copy ivars的,并且class_rw_t中并没有定义ivars的字段。 在访问某个成员变量时,直接通过isa_t找到对应的objc_class,并通过其class_ro_t的ivar list做地址偏移,查找对应的对象内存。正是由于这种方式,所以对象的内存地址是固定不可改变的。
- 方法调用
- 实例调用方法:objc_msgSend()发起调用,传入self和SEL –> 通过isa在类的内部查找方法列表对应的IMP; 如果调用的方法时涉及到当前对象的成员变量的访问,这时候就是在objc_msgSend()内部,通过类的ivar list判断地址偏移,取出ivar并传入调用的IMP中的。
- super调用父类方法:调用objc_msgSendSuper()函数实现。**需要注意的是,调用objc_msgSendSuper函数时传入的对象,也是当前实例变量,所以是在向自己发送父类的消息**
- RunTime中的结构体
- 常见结构体类型定义
typedef struct objc_class *Class; typedef struct objc_object *id; typedef struct method_t *Method; typedef struct ivar_t *Ivar; typedef struct category_t *Category; typedef struct property_t *objc_property_t;
- OC代码都是被首先编译问C语言文件然后生成二进制文件去执行。在OC中每个对象都是一个结构体(objc_object),结构体中都包含一个isa的成员变量,其位于成员变量的第一位。
struct objc_object { private: isa_t isa; };
OC的对象都会在编译过程中翻译为C语言的结构体:
NSArray *array = [[NSArray alloc] init]; ======================================= // @class NSArray; #ifndef _REWRITER_typedef_NSArray #define _REWRITER_typedef_NSArray typedef struct objc_object NSArray; typedef struct {} _objc_exc_NSArray; #endif NSArray *array = ((NSArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSArray"), sel_registerName("alloc")), sel_registerName("init"));
- OC中的类和元类也是一样,都是结构体构成的。由于类的结构体定义继承自objc_object,所以其也是一个对象,并且具有对象的isa特征;
- instaceOfType(objc_object) –(isa)–> isa_t –(class)–> 元类(objc_class)–superClass(isa)–> 根元类
- isa_t 类,描述对象相关的数据,cls字段指向元类
union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL struct { uintptr_t nonpointer : 1; // 是32位还是64位 uintptr_t has_assoc : 1; // 对象是否含有或曾经含有关联引用,如果没有关联引用,可以更快的释放对象 uintptr_t has_cxx_dtor : 1; // 表示是否有C++析构函数或OC的析构函数 uintptr_t shiftcls : 33; // 对象指向类的内存地址,也就是isa指向的地址 uintptr_t magic : 6; // 对象是否初始化完成 uintptr_t weakly_referenced : 1; // 对象是否被弱引用或曾经被弱引用 uintptr_t deallocating : 1; // 对象是否被释放中 uintptr_t has_sidetable_rc : 1; // 对象引用计数太大,是否超出存储区域 uintptr_t extra_rc : 19; // 对象引用计数 # define RC_ONE (1ULL<<45) # define RC_HALF (1ULL<<18) }; };
- 元类的数据结构(objc_class)
struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; class_data_bits_t bits; class_rw_t *data() { return bits.data(); } void setData(class_rw_t *newData) { bits.setData(newData); } // ..... }
- bits是objc_class的主角,其内部只定义了一个uintptr_t类型的bits成员变量,存储了class_rw_t的地址。和class_data_bits_t相关的有两个很重要结构体,class_rw_t和class_ro_t,其中都定义着method list、protocol list、property list等关键信息。在编译后class_data_bits_t指向的是一个class_ro_t的地址,这个结构体是不可变的(只读)。在运行时,才会通过realizeClass函数将bits指向class_rw_t。
struct class_rw_t { uint32_t flags; uint32_t version; const class_ro_t *ro; method_array_t methods; property_array_t properties; protocol_array_t protocols; Class firstSubclass; Class nextSiblingClass; char *demangledName; }; struct class_ro_t { uint32_t flags; uint32_t instanceStart; uint32_t instanceSize; uint32_t reserved; const uint8_t * ivarLayout; const char * name; method_list_t * baseMethodList; protocol_list_t * baseProtocols; const ivar_list_t * ivars; const uint8_t * weakIvarLayout; property_list_t *baseProperties; };
- Tagged Pointer
- 从iPhone5s开始,iOS设备开始引入了64位处理器,之前的处理器一直都是32位的。但是在64位处理器中,指针长度以及一些变量所占内存都发生了改变,32位一个指针占用4字节,但64位一个指针占用8字节;32位一个long占用4字节,64位一个long占用8字节等,所以在64位上内存占用会多出很多。苹果为了优化这个问题,推出了Tagged Pointer新特性。之前一个指针指向一个地址,而Tagged Pointer中一个指针就代表一个值。Tagged Pointer指针中就存储着对象的值;明显的提升了执行效率并节省了很多内存。在64位处理器下,内存占用减少了将近一半,执行效率也大大提升。由于通过指针来直接表示数值,所以没有了malloc和free的过程,对象的创建和销毁速度提升几十倍
- 对象的初始化流程
- alloc + init 和 new 两种调用方式在runtime;在runtime源码中,执行init操作本质上就是直接把self返回,其实如果没有自定义或者覆盖init,调不调用init都是一样的; 初始化对象:初始化isa指针,初始化元类,计算对象实例化时,所有变量所占的内存大小(对象内存大小固定(主要包括isa + ivar【class_ro_t】等)),在编译时就已经确定,在运行时无法改变
- 在对象销毁时,运行时环境会调用NSObject的dealloc方法执行销毁代码,并不需要我们手动去调用。接着会调用到Runtime内部的objc_object::rootDealloc(C++命名空间)函数。
- class_addMethod runTime过程中动态的添加方法
- 在Runtime中动态的替换或者增加方法,本质上都是调用addMethod方法;该方法会返回一个布尔值,用来表明所要添加的方法是否存在
- 在分类的load方法中进行方法swizzle的步骤
- 声明原始方法和swizzle方法(在分类中实现了)的SEL和Method
- 调用class_Method方法,添加原始方法为方法名,swizzle实现为Method的方法
- 若class_Method调用返回True,说明原始方法不存在并且调价成功,那么调用class_replaceMethod替换方法名为swizzle方法的实现为原始Method的方法
- 若class_Method调用返回false,说明原始方法已经存在添加失败,那么调用method_exchangeImplementations交换原始和swizzle的方法实现Method
Class class = [self class]; SEL originalSelector = @selector(dismissViewControllerAnimated:completion:); SEL swizzledSelector = @selector(dfup_dismissViewControllerAnimated:completion:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }
- SEL是由objc_selector结构体实现的,但是从现在的源码来看,SEL是一个const char*的常量字符串,只是代表一个名字而已
- 程序加载过程
- 在iOS程序中会用到很多系统的动态库,这些动态库都是动态加载的。所有iOS程序共用一套系统动态库,在程序开始运行时才会开始链接动态库。
- 在应用程序启动后,由dyld(the dynamic link editor)进行程序的初始化操作。大概流程就像下面列出的步骤,其中第3、4、5步会执行多次,在ImageLoader加载新的image进内存后就会执行一次。
- 在应用程序启动后,由dyld将应用程序加载到二进制中,并完成一些文件的初始化操作。
- Runtime向dyld中注册回调函数。
- 通过ImageLoader将所有image加载到内存中。
- dyld在image发生改变时,主动调用回调函数。
- Runtime接收到dyld的函数回调,开始执行map_images、load_images等操作,并回调+load方法。
- 调用main()函数,开始执行业务代码。
- load
- load类方法的调用时机比main函数还要靠前。load方法是由系统来调用的,并且在整个程序运行期间,只会调用一次,所以可以在load方法中执行一些只执行一次的操作
- load方法的调用顺序应该是“父类 -> 子类 -> 分类”的顺序。因为执行加载Class的时机是在Category之前的,而且load子类之前会先load父类,所以是这种顺序
- 但是若子类没有实现load并不会去掉用父类的
- initialize
- 和load方法类似的也有initialize方法,initialize方法也是由Runtime进行调用的,自己不可以直接调用。与load方法不同的是,initialize方法是在第一次调用类所属的方法(实例方法、类方法)时,才会调用initialize方法,而load方法是在main函数之前就全部调用了。所以理论上来说initialize可能永远都不会执行,如果当前类的方法永远不被调用的话。
- 分类的实现的initialize方法会覆盖本类的实现
- 如果没有主动的显式的去掉用initialize,initialize只会被调用一次
- 消息的发送机制
- 在OC中方法调用是通过Runtime实现的,Runtime进行方法调用本质上是发送消息,通过objc_msgSend()函数进行消息发送
- 不同类的相同SEL是同一个对象。
- 当一个对象被创建时,系统会为其分配内存,并完成默认的初始化工作,例如对实例变量进行初始化。对象第一个变量是指向其类对象的指针-isa,isa指针可以访问其类对象,并且通过其类对象拥有访问其所有继承者链中的类
- 消息发送的流程
- 判断当前调用的SEL是否需要忽略
- 判断接收消息的对象是否为nil
- 从方法的缓存列表中查找,通过cache_getImp函数进行查找,如果找到缓存则直接返回IMP。
- 查找当前类的method list,查找是否有对应的SEL,如果有则获取到Method对象,并从Method对象中获取IMP,并返回IMP(这步查找结果是Method对象)。
- 如果在当前类中没有找到SEL,则去父类中查找。首先查找cache list,如果缓存中没有则查找method list,并以此类推直到查找到NSObject为止。
- 如果在类的继承体系中,始终没有查找到对应的SEL,则进入动态方法解析中。可以在resolveInstanceMethod和resolveClassMethod两个方法中动态添加实现。
- 动态消息解析如果没有做出响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一些处理,否则就会Crash。
- Category
- Category语法很相似的还有Extension,二者的区别在于,Extension在编译期就直接和原类编译在一起,而Category是在运行时动态添加到原类中的
- 在有多个Category和原类的方法重复定义的时候,原类和所有Category的方法都会存在,并不会被后面的覆盖。假设有一个方法叫做method,Category和原类的方法都会被添加到方法列表中,只是存在的顺序不同;在进行方法调用的时候,会优先遍历Category的方法,并且后面被添加到项目里的Category,会被优先调用
-
元类中的MethodList的最终顺序**同时与Building Phases的编译列表和是否实现了+load方法有关**
- 没有实现+load方法的分类方法会排在实现了+load前面
- 若同为实现或者同为未实现+load方法的分类,则按照Building Phases的编译列表中的顺序排列
-
元类中的MethodList的最终顺序**同时与Building Phases的编译列表和是否实现了+load方法有关**
-
在有多个Category和原类方法重名的情况下,怎样在一个Category的方法被调用后,调用所有Category和原类的方法?
可以在一个Category方法被调用后,遍历方法列表并调用其他同名方法。但是需要注意一点是,遍历过程中不能再调用自己的方法,否则会导致递归调用。为了避免这个问题,可以在调用前判断被调动的方法IMP是否当前方法的IMP。
- (void)method { //获取当前方法的IMP IMP currentIMP = [[self class] instanceMethodForSelector:_cmd]; unsigned int methodCount = 0; //获取类的所有方法列表(Methodlist)包括私有方法 Method *methodList = class_copyMethodList([self class], &methodCount); NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount]; //定义IMP函数指针类型 void (*function) (id self, SEL _cmd, NSObject* object); for(int i = 0; i < methodCount; i++) { Method temp = methodList[i]; IMP imp = method_getImplementation(temp); SEL name_f = method_getName(temp); //判断与当前函数同名且不是当前函数的IMP if ([[NSString stringWithUTF8String:sel_getName(name_f)] isEqualToString:[NSString stringWithUTF8String:sel_getName(_cmd)]] && imp != currentIMP) { function = method_getImplementation(temp); function(self, name_f, [NSObject new]); } } }
-
那怎样在任何一个Category的方法被调用后,只调用原类方法呢?
根据上面对方法调用的分析,Runtime在调用方法时会优先所有Category调用,所以可以倒叙遍历方法列表,只遍历第一个方法即可,这个方法就是原类的方法。
- (void)method { NSLog(@"printMySelf --- MainClass (C)"); //获取当前方法的IMP IMP currentIMP = [[self class] instanceMethodForSelector:_cmd]; unsigned int methodCount = 0; //获取类的所有方法列表(Methodlist)包括私有方法 Method *methodList = class_copyMethodList([self class], &methodCount); NSMutableArray *methodsArray = [NSMutableArray arrayWithCapacity:methodCount]; //定义IMP函数指针类型 void (*function) (id self, SEL _cmd, NSObject* object); //将MethodList从后向前遍历,找到第一个同名Method,即为 for(int i = methodCount - 1; i >=0 ; i--) { Method temp = methodList[i]; IMP imp = method_getImplementation(temp); SEL name_f = method_getName(temp); //判断不是当前函数的IMP if ([[NSString stringWithUTF8String:sel_getName(name_f)] isEqualToString:[NSString stringWithUTF8String:sel_getName(_cmd)]] && imp != currentIMP) { function = method_getImplementation(temp); function(self, name_f, [NSObject new]); break; } } }
- 消息的转发
- 当通过调用objc_msgSend()方法调用,也无法找到对应的Method的时候;会进入动态转发流程
- +resolveInstanceMethod:动态解析类方法;可以在该方法中调用class_addMethod动态的添加方法,返回True
- forwardingTargetForSelector:(SEL)aSelector:可以返回备援对象的实例
- forwardInvocation:(NSInvocation *)anInvocation 作用与上一步等效,new其他备援对象的实例,作为NSInvocation调用的target
- 当通过调用objc_msgSend()方法调用,也无法找到对应的Method的时候;会进入动态转发流程
- KVO
- KVO和NSNotificationCenter都是iOS中观察者模式的一种实现。区别在于,相对于被观察者和观察者之间的关系,KVO是一对一的,而不一对多的。KVO对被监听对象无侵入性,不需要修改其内部代码即可实现监听
- 通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件
- 在调用addObserver方法后,KVO并不会对观察者进行强引用,所以需要注意观察者的生命周期,否则会导致观察者被释放带来的Crash
- 观察者需要实现observeValueForKeyPath:ofObject:change:context:方法,当KVO事件到来时会调用这个方法,如果没有实现会导致Crash
- UIView
- frame实现相对父View坐标系中的位置描述;bounds是对于自身坐标系的表描述,bounds中position是对左上角的坐标点的位置的描述,即对其所有子View的Frame的参考坐标,所以父View的bounds的position的改变会影响所有子View的位置。UIScrollView滑动的时候实际改变的是bounds的position。
- layoutSubviews方法:init初始化不会触发layoutSubviews;addSubview会触发layoutSubviews;frame发生变化会触发layoutSubviews;滚动一个UIScrollView会触发layoutSubviews;改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件;创建一个customView后,必须找到着陆床,让它能够被addSubview到一个父层,否则永远不会进入layoutSubviews
- how to use updateConstraints
- 尽量将约束的添加写到类似于viewDidLoad的方法中(init方法)。
- updateConstraints并不应该用来给视图添加约束,它更适合用于周期性地更新视图的约束,或者在添加约束过于消耗性能的情况下将约束写到该方法中。
- 当我们在响应事件时(例如点击按钮时)对约束的修改如果写到updateConstraints中,会让代码的可读性非常差。
- 刷新子对象布局
- layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
- setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
- layoutIfNeeded方法:如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
- 重绘
- drawRect:(CGRect)rect方法:重写此方法,执行重绘任务
- 该方法在调用sizeToFit后被调用
- drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法
- 要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕
- setNeedsDisplay方法:标记为需要重绘,异步调用drawRect
- setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘
- sizeToFit
- sizeToFit会自动调用sizeThatFits方法;
- sizeToFit不应该在子类中被重写,应该重写sizeThatFits
- sizeThatFits传入的参数是receiver当前的size,返回一个适合的size
- drawRect:(CGRect)rect方法:重写此方法,执行重绘任务
- UIView的绘制
- 内存管理