絮:2020年到来了,随着疫情的到来,我也失业了。一边忙着抵抗病毒,一边还得继续准备面试。又该准备“造火箭”了,去了继续“拧螺丝”,下面是自己最近的一些总结,也会在后面的面试中,遇到的面试问题,也一并记录一下。
如有编写有问题,可以留言。不可避免的有错别字,望见谅!
如果您也是个面试者,碰到面问题,可以留言、私信交流一下。
如果你还想看其他面试题,可以移步到2017年面试题
1. ARC帮我们做了什么?
- 使用LLVM + Runtime 结合帮我管理对象的生命周期
- LLVM 帮我们在代码合适的地方添加
release
、retarn
、autorelease
等添加计数器或者减少计数器操作 - Runtime 帮我们像
__weak
、copy
等关键字的操作
2.initialize
和load
是如何调用的?它们会多次调用吗?
-
load
方法说在应用加载的时候,Runtime
直接拿到load
的IMP
直接去调用的,而不是像其他方式根据objc_msgSend
(消息机制)来调用方法的-
load
方法调用的顺序是根据类的加载的前后进行调用的,但是每个类调用的顺序是superclass->class->category
顺序调用的,每个load
方法只会调用一次(手动调用不算) - 一下为
Runtime
源码的主要代码
load_images(const char *path __unused, const struct mach_header *mh) { // 准备class 和category prepare_load_methods((const headerType *)mh); // 调用load方法 call_load_methods(); } void prepare_load_methods(const headerType *mhdr) { classref_t *classlist = _getObjc2NonlazyClassList(mhdr, &count); for (i = 0; i < count; i++) { schedule_class_load(remapClass(classlist[i])); } category_t **categorylist = _getObjc2NonlazyCategoryList(mhdr, &count); for (i = 0; i < count; i++) { category_t *cat = categorylist[i]; add_category_to_loadable_list(cat); } } static void schedule_class_load(Class cls) { // 开始递归,加载superclass schedule_class_load(cls->superclass); add_class_to_loadable_list(cls); } void call_load_methods(void) { do { while (loadable_classes_used > 0) { call_class_loads(); } more_categories = call_category_loads(); } while (loadable_classes_used > 0 || more_categories); } static void call_class_loads(void) { // 在此add_class_to_loadable_list 里面准备了所有重写load的方法的类 struct loadable_class *classes = loadable_classes; // Call all +loads for the detached list. for ( int i = 0; i < used; i++) { Class cls = classes[i].cls; // 获取到load 方法的imp load_method_t load_method = (load_method_t)classes[i].method; // 调用laod 方法 (*load_method)(cls, SEL_load); } } static bool call_category_loads(void) { // 在prepare_load_methods 方法里面准备了所有重新load方法的category struct loadable_category *cats = loadable_categories; for (int i = 0; i < used; i++) { // 获取到catgegory Category cat = cats[i].cat; // 获取category 的load 方法的IMP实现 load_method_t load_method = (load_method_t)cats[i].method; cls = _category_getClass(cat); if (cls && cls->isLoadable()) { // 调用load方法 (*load_method)(cls, SEL_load); } } }
-
initialize
方法的调用其实和其他方法调用一样的,objc_msgSend
(消息机制)来调用的。调用的数序是:没有初始话的superclass ->
实现initialize
的categort
或者 实现了initialize
的class
,如果class
没有实现initialize
方法,则会调用superclass
的initialize
,因为initialize
的底层是使用了objc_msgSend
- 看下
Runtime
底层调用_class_initialize
的源码
void _class_initialize(Class cls) { supercls = cls->superclass; if (supercls && !supercls->isInitialized()) { // 又是个递归 _class_initialize(supercls); } // 调用 initialize方法 callInitialize(cls); } // objc_msgSend 调用 initialize 方法 void callInitialize(Class cls) { // **注意:因为使用了objc_msgSend,有可能调用class的 initialize ** objc_msgSend(cls, SEL_initialize); }
总结:
load
方法一个类只会调用一次(除去手动调用),而调用的数序是,从superclass -> class -> category
,category
里面的顺序是先编译,先调用
initialize
方法,一个类可能会调用多次,如果子类没有实现initialize
方法,当第一次使用此类的时候,会调用superclass
。而调用的顺序是,superclass -> 实现initialize的category 或者 实现了initialize方法(没有category实现initialize) 或者 superclass的initialize (没有子类和category实现initialize方法)
-
3.说下autoreleasepool
-
在
MRC
下,当对象调用autorerelease
方法时候,会将对象加入到对象前面的哪一个autoreleasepool
里面,并且当autoreleasepool
作用域释放的时候,会对里面的所有的对象进行一次release
操作。-
autoreleasepool
底层是使用了AutoreleasePoolPage
对象来管理的,AutoreleasePoolPage
是一个双向的链表,每个AutoreleasePoolPage
都有4096个字节,除了用来存放内部的成员变量,剩下的控件都会用来存放autorelease
对象的地址/// AutoreleasePoolPage 的简化的结构 class AutoreleasePoolPage { magic_t const magic; // 下一次可以存储对象的地址 id *next; pthread_t const thread; // 标识上一个page对象 AutoreleasePoolPage * const parent; // 标识下一个page对昂 AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat; }
- 当
autoreleasepool
开始的时候,会调用AutorelasePoolPage
的push
方法,会讲一个标识POOL_BOUNDARY
添加到AutoreleasePoolPage
对象里面,并且返回POOL_BOUNDARY
的地址r1
(暂且这样叫) - 当对像进行
relase
的时候,会将对象的地址添加到当前AutorelasePoolPage
里面,依次添加。 - 当
autoreleasepool
作用域结束的时候,会调用AutorelasePoolPage
的pop(r1)
方法(r1为当前aotoreleasepool开始的加入标识POOL_BOUNDARY的地址),AutorelasePoolPage
则会将里面保存的对象的从左后一个开始进行release
操作,当碰到r1
时候,标识当前那个autoreleasepool
里面所有的对象都进行了一次release
操作。
@autoreleasepool { // 此处会调用 void *ctxt = AutoreleasePoolPage::push(); // 添加到最近的一个autoreleasepool中 [[[NSObject alloc]init] autorelease]; //移除作用域的时候调用 AutoreleasePoolPage:pop(ctxt) } // autoreleasepool 作用域开始会调用AutoreleasePoolPage::push() static inline void *push() { id *dest; if (DebugPoolAllocation) { // 创建一个心的page对象 dest = autoreleaseNewPage(POOL_BOUNDARY); } else { // 已经有了page对象,讲`pool_boundary`添加进去 dest = autoreleaseFast(POOL_BOUNDARY); } } static inline id *autoreleaseFast(id obj) { // 获取正在使用的page对昂 AutoreleasePoolPage *page = hotPage(); // page还没有装满 if (page && !page->full()) { return page->add(obj); } else if (page) { // 已经添加满了 return autoreleaseFullPage(obj, page); } else { // 没有page对象,创建心的page对象 return autoreleaseNoPage(obj); } } // 对象调用release 的简介源码 id objc_object::rootAutorelease2() { return AutoreleasePoolPage::autorelease((id)this); } static inline id autorelease(id obj) { // 同样也是添加进去 id *dest = autoreleaseFast(obj); return obj; } // page调用pop简介源码 *token 表示结束的标识 static inline void pop(void *token) { AutoreleasePoolPage *page; id *stop; page = pageForPointer(token); stop = (id *)token; page->releaseUntil(stop); } // 释放对象的源码 void releaseUntil(id *stop) { // next 标识当前page可以存储对象的下一个地址 while (this->next != stop) { AutoreleasePoolPage *page = hotPage(); // 因为page是个双向链表,当page为空的时候,需要往上查找parent的page对象里面存储的睇相 while (page->empty()) { page = page->parent; setHotPage(page); } id obj = *--page->next; if (obj != POOL_BOUNDARY) {// obj 不是刚开始传入的POOL_BOUNDARY及表示对象,所以需要调用一次操作 objc_release(obj); } } }
-
-
autoreleasepool
和runloop
的关系-
runloop
里面会注册两个Observer
来监听runloop
的状态变化- 其中一个
Observer
监听的状态为kCFRunLoopEntry
进入runloop的状态,则会调用AutoreleasePoolPage::push()
方法 - 另外中一个
Observer
监听的状态为kCFRunLoopBeforeWaiting、kCFRunLoopExit
,即将休眠和退出当前的runloop。- 在
kCFRunLoopBeforeWaiting
的回掉里面会调用AutoreleasePoolPage::pop(ctxt)和AutoreleasePoolPage::(push)
方法,释放上一个autoreleasepool
里面添加的对象,并开启下一个autoreleasepool
。 - 在
kCFRunLoopExit
的Observer
回掉里面会调用AutoreleasePoolPage::(push)
释放autoreleasepool
里面的对象
- 在
- 其中一个
-
4.category
属性是存储在那里?
- 我们都知道可以使用
Runtime
的objc_setAssociatedObject
、objc_getAssociatedObject
两个方法给category
的属性重写get
、set
方法,而此属性的值是存储在那里呢? - 其实此属性的值保存在一个
AssociationsManager
里面。 - 我们也是可以根据源码看一下
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// 一下为精简的代码
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
}
}
}
5.category
方法是如何添加的?
- 当我们给分类添加相同的方法的时候,会调用
category
里面的方法,而不是调用我们class
里面的方法 - 当编译器编译的时候,编译器会将
category
编译成category_t
这样的结构体,等类初始化的时候,会将分类的信息同步到class_rw_t
里面,包含:method、property、protocol
等,同步的时候会将category
里面的信息添加到class
的前面(而不是替换掉class里面的方法),而方法调用的时候,而是遍历class_rw_t
里面的方法,所以找到分类里面的IMP
则返回。- 使用
memmove
,将类方法移动到后面 - 使用
memcpy
,将分类的方法copy到前面
- 使用
- 当多个分类有相同的方法的时候,调用的顺序是
后编译先调用
- 当类初始化同步
category
的时候,会使用while(i--)
的倒序循环,将后编译的category
添加到最前面。
- 当类初始化同步
6. OC 的消息机制
- 消息机制可以分为三个部分
- 消息传递
- 当我么调用方法的时候,方法的调用都会转化为
objc_msgSend
这样来传递。 - 第一步会根据对象的
isa
指针找到所属的类(也就是类对象) - 第二步,会根据类对象里面的
catch
里面查找。catch
是个散列表,是根据@selector(方法名)
来获取对应的IMP
,从而开始调用 - 第三步,如果第二步没有找到,会继续查找到类对象里面的
class_rw_t
里面的methods
(方法列表),从而遍历,找到方法所属的IMP
,如果查找到则会添加到catch
表里面 - 第四步,如果第三部也没有找到,会根据类对象里面的
superclass
指针,查找super
的catch
,如果也是没有查找,会继续查找到superclass
里面的class_rw_t
里面的methods
(方法列表),从而遍历,找到方法所属的IMP
,如果查找到则会添加到catch
表里面 - 第五步,如果第四部还是没有查找到,此时会根据类的
superclass
,继续第四部操作 - …………
- 第六步。如果一直查找到基类都没有找到响应的方法,则会进入动态解析里面
- 当我么调用方法的时候,方法的调用都会转化为
- 动态解析
- 当消息传递,没有找到对应的
IMP
的时候,会进入的动态解析中 - 此时会根据方法是类方法,还是实例方法分别调用
+(BOOL)resolveClassMethod:(SEL)sel
、+(BOOL)resolveInstanceMethod:(SEL)sel
- 我们可以实现这两个方法,使用
Runtime的class_addMethod
来添加对应的IMP
- 如果添加后,返回
true
,没有添加则调用父类方法 - 注意:其实返回
true
或者false
,结果都是一样的,再次掉消息传递步骤
- 当消息传递,没有找到对应的
- 消息转发
- 如果我们没有实现动态解析方法,就会走到消息转发这里
- 第一步,会调用
-(id)forwardingTargetForSelector:(SEL)aSelector
方法,我们可以在这里,返回一个响应aSelector
的对象。当返回不为nil
时候,系统会继续再次走消息转发,继续查找对应的IMP
- 第二步,如果第一步返回
nil或者self(自己)
,此时系统会继续走这里-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
,需要返回aSelector
的一个签名 - 第三步,如果返回了签名,就会到这里
-(void)forwardInvocation:(NSInvocation *)anInvocation
,相应的我们可以根据anInvocation
,可以获取到参数、target、方法名
等,再次操作的空间就很多了,看你需求喽。此时我们什么都不操作也是没问题的, - 注意:当我们是类方法的时候,其实我们可以将以上方法的
-
改为+
,即可实现了类方法的转发
- 消息传递
7.weak
表是如何存储__weak
指针的
-
weak
关键字,我们都知道,当对象销毁的时候,也会将指针赋值为nil
,而weak
的底层也是将指针和对象以键值对的形式存储在哈希表
里面 - 当使用
__weak
修饰的时候,底层会调用id objc_storeWeak(id *location, id newObj)
传递两个参数- 第一个参数为指针,第二个参数为所指向的对象
- 第二步,继续调用
storeWeak(location, (objc_object *)newObj)
- 第一个参数是指针,第二个参数是对象的地址
- 再次方法里面会根据
对象地址生成一个SideTables对象
- 第三步,调用
id weak_register_no_lock(weak_table_t *weak_table, id referent_id, id *referrer_id, bool crashIfDeallocating)
-
weak_table
则为SideTables的一个属性
,referent_id
为对象,referrer_id
则为那个弱引用的指针 - 在此里面会根据
对象地址和指针
生成一个weak_entry_t
-
- 第四步,会继续调用
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
- 重点:在此方法里面会根据对象 &
weak_table->mask
(表示weak表里面可以存储的大小减一,例如:表可以存储10个对象,那么mask就是9), 生成对应的index,如果index对应已经存储上对象,则会index++的方式找到未存储的对应,并将new_entry
存储进去,储存在weak_table里的weak_entries
属性里面
- 重点:在此方法里面会根据对象 &
- 注意:当一个对象多个
weak
指针指向的时候,生成的也是一个entry
,多个指针时保存在entry
里面referrers
属性里面 - 以下为简易的源码:
id
objc_storeWeak(id *location, id newObj)
{
return storeWeak
(location, (objc_object *)newObj);
}
static id
storeWeak(id *location, objc_object *newObj) {
// 根据对象生成新的SideTable
SideTable *newTable = &SideTables()[newObj];
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
}
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating){
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;
// 根据对象和指针生成一个entry
weak_entry_t new_entry(referent, referrer);
// 检查是是否该去扩容
weak_grow_maybe(weak_table);
// 将新的entry 插入到表里面
weak_entry_insert(weak_table, &new_entry);
}
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;
size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
size_t index = begin;
size_t hash_displacement = 0;
while (weak_entries[index].referent != nil) {
index = (index+1) & weak_table->mask;
if (index == begin) bad_weak_table(weak_entries);
hash_displacement++;
}
weak_entries[index] = *new_entry;
weak_table->num_entries++;
}
weak_table
的扩容,根据存储条数>=
最大存储条数的3/4
时,就会按照两倍
的方式进行扩容,并且会将已经有的条目再次生成新的index
(因为扩容后,weak_table的mask
发生了改变)。进行保存
- 以下为简易的源码:
static void weak_grow_maybe(weak_table_t *weak_table)
{
size_t old_size = (weak_table->mask ? weak_table->mask + 1 : 0);
if (weak_table->num_entries >= old_size * 3 / 4) {
weak_resize(weak_table, old_size ? old_size*2 : 64);
}
}
static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
size_t old_size = TABLE_SIZE(weak_table);
weak_entry_t *old_entries = weak_table->weak_entries;
// calloc 分配新的控件
weak_entry_t *new_entries = (weak_entry_t *)
calloc(new_size, sizeof(weak_entry_t));
// mask 就是大小减一
weak_table->mask = new_size - 1;
weak_entry_t *entry;
weak_entry_t *end = old_entries + old_size;
for (entry = old_entries; entry < end; entry++) {
if (entry->referent) {
weak_entry_insert(weak_table, entry);
}
}
}
8. 方法catch
表是如何存储方法的
- 我们都是知道调用方法的时候,会根据对象的
isa
查找到对象类对象
,并开始在catch
表里面查询对应的IMP
- 其实
catch
是个散列表
,是根据方法的@selector(方法名) & catch->mask(catck表最大数量 - 1)
得到index,如果index已经存储了新的方法,那么就会index++
,如果index
对应的值为nil
时,将响应的方法,插入到catch
表里面 - 核心代码
static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
// 获取类对象的catch地址
cache_t *cache = &cls->cache
// 获取key
cache_key_t key = (cache_key_t)sel;
// 找到bucket
bucket_t *bucket = cache->find(key, receiver);
}
bucket_t * cache_t::find(cache_key_t k, id receiver)
{
// catch表的buckets属性
bucket_t *b = buckets();
// catch 表示的mask 最大值 - 1
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0 || b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);
}
static inline mask_t cache_next(mask_t i, mask_t mask) {
return (i+1) & mask;
}
注意:
catch
表的扩容,同样也是和weak_table
一样按照2倍
的方式进行扩容,但是注意:扩容后,以前缓存的方法则会被删除掉。
- 简易代码
void cache_t::expand() {
uint32_t oldCapacity = capacity();
uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE;
reallocate(oldCapacity, newCapacity);
}
void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity)
{
// 获取旧的oldBuckets
bucket_t *oldBuckets = buckets();
// 重新分配新的
bucket_t *newBuckets = allocateBuckets(newCapacity);
// free 掉旧的
cache_collect_free(oldBuckets, oldCapacity);
}
9.优化后isa
指针是什么样的?存储都有哪些内容?
- 最新的
Objective-C
的对象里面的isa
指针已经不是单单的指向所属类的地址了的指针了,而时变成了一个共用体
,并且使用位域
来存储更多的信息
10.App启动流程,以及如何优化?
-
启动顺序
- dyld,Apple的动态连接器,可以用来装载
Mach-O
文件(可执行文件、动态库)- 装载App的可执行文件,同事递归加载所有依赖的动态库
- 当dyld把可执行文件、动态库装载完毕后,会通知
Runtime
进行下一步的处理
- Runtime
- 调用
map_images
进行可执行文件内容的解析和处理 - 在
load_images
里面调用call_load_methods
,调用所有class和category的+load
方法 - 进行各种
objc
结构的初始化(注册Objc类,初始化类对象等等) - 到目前未知,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP..)都已经按照格式成功加载到内存中,被
runtime
管理
- 调用
- main函数调用
- 所有初始化工作结束后,dyld就会调用
main
函数 - 截下来就是
UIApplicationMan
函数,AppDelegate
的application:didFinishLaunchingWithOptions:
的
- 所有初始化工作结束后,dyld就会调用
- dyld,Apple的动态连接器,可以用来装载
-
App启动速度优化
-
dyld
- 减少动态库,合并一些自定义的动态库,以及定期清理一些不需要的动态库
- 较少
Objc
类、category
的数量、以及定期清理一些不必要的类和分类 - Swift尽量使用
struct
-
Runtime
- 使用
+initialize
和dispatch_once
取代Objc的+load
方法、C++的静态构造器
- 使用
-
main
- 再不印象用户体验的情况下面,尽可能的将一些操作延迟,不要全部放到
finishLaunching
- 一些网络请求
- 一些第三方的注册
- 以及
window的rootViewController
的viewDidload
方法,也别做耗时操作
- 再不印象用户体验的情况下面,尽可能的将一些操作延迟,不要全部放到
-
注意:我们可以添加环境变量可以打印出App的启动时间分析(
Edit scheme -> Run -> Arguments
)-
DYLD_PRINT_STATISTICS
设置为1,可以打印出来每个阶段的时间 - 如果需要更详细的信息,那就设置
DYLD_PRINT_STATISTICS_DETAILS
为1
-
-
11.App瘦身
-
资源(图片、音频、视频等)
- 可以采取无损压缩
- 使用
LSUnusedResources
去除没有用的资源 LSUnusedResources
-
可执行文件瘦身
-
Strip Linked Product
、Make Strings Read-Only
、Symbols Hidden by Default
设置为true
- 去掉一些异常支持
Enable C++ Exceptions
、Enable Objective-C Exceptions
设置为false
- 使用
AppCode
检测未使用的代码:菜单栏 -> Code -> Inspect Code
,等编译完成后,会看到未使用的类
-
-
生成
LinkMap
文件,可以查看可执行文件的具体组成- 可借助第三方工具解析
LinkMap
文件LinkMap
- 可借助第三方工具解析
Link Map
解析结果
如果招聘者看到了,感觉我还可以,欢迎您的私信一下
如果招聘者看到了,感觉我还可以,欢迎您的私信一下
如果招聘者看到了,感觉我还可以,欢迎您的私信一下
持续更新中。。。。