去年随着疫情的到来,我也失业了。一边忙着抵抗病毒,一边还得继续准备面试。又该准备“造火箭”了,去了继续“拧螺丝”,下面是自己最近的一些总结,也会在后面的面试中,遇到的面试问题,也一并记录一下。

如有编写有问题,可以留言。不可避免的有错别字,望见谅!

如果您也是个面试者,碰到面问题,可以留言、私信交流一下。

如果你还想看其他面试题,可以移步到2017年面试题

1.ARC帮我们做了什么?

使用LVVM + Runtime 结合帮我管理对象的生命周期

LVVM 帮我们在代码合适的地方添加release、retarn、autorelease等添加计数器或者减少计数器操作

Runtime 帮我们像__weak、copy等关键字的操作

2.initialize和load是如何调用的?它们会多次调用吗?

load方法说在应用加载的时候,Runtime直接拿到load的IMP直接去调用的,而不是像其他方式根据objc_msgSend(消息机制)来调用方法的

load_images(const char *path __unused, const struct mach_header *mh) {// 准备class 和categoryprepare_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) {// 开始递归,加载superclassschedule_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 方法的impload_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方法的categorystruct loadable_category *cats = loadable_categories;for (int i = 0; i < used; i++) {// 获取到catgegoryCategory 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);}}}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方法)

initialize方法的调用其实和其他方法调用一样的,objc_msgSend(消息机制)来调用的。调用的数序是:没有初始话的superclass -> 实现initialize的categort 或者 实现了initialize的class,如果class没有实现initialize 方法,则会调用superclass的initialize,因为initialize的底层是使用了objc_msgSend

看下Runtime底层调用_class_initialize的源码

load方法调用的顺序是根据类的加载的前后进行调用的,但是每个类调用的顺序是superclass->class->category顺序调用的,每个load方法只会调用一次(手动调用不算)

一下为Runtime源码的主要代码

3.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);}}}

4.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添加到最前面。

5. OC 的消息机制

消息机制可以分为三个部分

如果我们没有实现动态解析方法,就会走到消息转发这里

第一步,会调用-(id)forwardingTargetForSelector:(SEL)aSelector方法,我们可以在这里,返回一个响应aSelector的对象。当返回不为nil时候,系统会继续再次走消息转发,继续查找对应的IMP

第二步,如果第一步返回nil或者self(自己),此时系统会继续走这里-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,需要返回aSelector的一个签名

第三步,如果返回了签名,就会到这里-(void)forwardInvocation:(NSInvocation *)anInvocation,相应的我们可以根据anInvocation,可以获取到参数、target、方法名等,再次操作的空间就很多了,看你需求喽。此时我们什么都不操作也是没问题的,

注意:当我们是类方法的时候,其实我们可以将以上方法的-改为+,即可实现了类方法的转发

当消息传递,没有找到对应的IMP的时候,会进入的动态解析中

此时会根据方法是类方法,还是实例方法分别调用+(BOOL)resolveClassMethod:(SEL)sel、+(BOOL)resolveInstanceMethod:(SEL)sel

我们可以实现这两个方法,使用Runtime的class_addMethod来添加对应的IMP

如果添加后,返回true,没有添加则调用父类方法

注意:其实返回true或者false,结果都是一样的,再次掉消息传递步骤

当我么调用方法的时候,方法的调用都会转化为objc_msgSend这样来传递。

第一步会根据对象的isa指针找到所属的类(也就是类对象)

第二步,会根据类对象里面的catch里面查找。catch是个散列表,是根据@selector(方法名)来获取对应的IMP,从而开始调用

第三步,如果第二步没有找到,会继续查找到类对象里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面

第四步,如果第三部也没有找到,会根据类对象里面的superclass指针,查找super的catch,如果也是没有查找,会继续查找到superclass里面的class_rw_t里面的methods(方法列表),从而遍历,找到方法所属的IMP,如果查找到则会添加到catch表里面

第五步,如果第四部还是没有查找到,此时会根据类的superclass,继续第四部操作

…………

第六步。如果一直查找到基类都没有找到响应的方法,则会进入动态解析里面

消息传递、动态解析、消息转发

6.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属性里面

以下为简易的源码:

idobjc_storeWeak(id *location, id newObj){return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>(location, (objc_object *)newObj);}static idstoreWeak(id *location, objc_object *newObj) {// 根据对象生成新的SideTableSideTable *newTable = &SideTables()[newObj];newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, (id)newObj, location,  crashIfDeallocating);}idweak_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;// 根据对象和指针生成一个entryweak_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);}}}

7. 方法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// 获取keycache_key_t key = (cache_key_t)sel;// 找到bucketbucket_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 最大值 - 1mask_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){// 获取旧的oldBucketsbucket_t *oldBuckets = buckets();// 重新分配新的bucket_t *newBuckets = allocateBuckets(newCapacity);// free 掉旧的cache_collect_free(oldBuckets, oldCapacity);}

8.优化后isa指针是什么样的?存储都有哪些内容?

最新的Objective-C的对象里面的isa指针已经不是单单的指向所属类的地址了的指针了,而时变成了一个共用体,并且使用位域来存储更多的信息

#9.App启动流程,以及如何优化?

启动顺序

所有初始化工作结束后,dyld就会调用main函数

截下来就是UIApplicationMan函数,AppDelegate的application:didFinishLaunchingWithOptions:的

调用map_images进行可执行文件内容的解析和处理

在load_images里面调用call_load_methods,调用所有class和category的+load方法

进行各种objc结构的初始化(注册Objc类,初始化类对象等等)

到目前未知,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP…)都已经按照格式成功加载到内存中,被runtime管理

装载App的可执行文件,同事递归加载所有依赖的动态库

当dyld把可执行文件、动态库装载完毕后,会通知Runtime进行下一步的处理

dyld,Apple的动态连接器,可以用来装载Mach-O文件(可执行文件、动态库)

Runtime

main函数调用

App启动速度优化

DYLD_PRINT_STATISTICS设置为1,可以打印出来每个阶段的时间

如果需要更详细的信息,那就设置DYLD_PRINT_STATISTICS_DETAILS为1

再不印象用户体验的情况下面,尽可能的将一些操作延迟,不要全部放到finishLaunching

以及window的rootViewController 的viewDidload方法,也别做耗时操作

一些网络请求

一些第三方的注册

使用+initialize和dispatch_once取代Objc的+load方法、C++的静态构造器

减少动态库,合并一些自定义的动态库,以及定期清理一些不需要的动态库

较少Objc类、category的数量、以及定期清理一些不必要的类和分类

Swift尽量使用struct

dyld

Runtime

main

注意:我们可以添加环境变量可以打印出App的启动时间分析(Edit scheme -> Run -> Arguments)

10.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解析结果

原文链接:https://xw.qq.com/cmsid/20200307A05DH700

过去的一年iOS面试总结相关推荐

  1. 「iOS 面试之道」勘误(二)

    本文是「iOS 面试之道」勘误系列的第二篇. Swift 初始化方法描述的漏洞 书中 105 页的描述是:Swift 初始化方法必须保证所有非 Optional 的成员变量都完成初始化. 这个表述是不 ...

  2. android 无appid分享_App ID 和Bundle ID 有什么不同?ios面试攻克篇(六)

    '写在前面的话' 这些是我对iOS面试时会碰到的问题的解决方法,整理出来分享给大家,有些错误不要 笑了,希望对大家有所帮助.大家有更好的解决办法也欢迎沟通交流. 一直以来都有写点儿东西的想法,就从笔记 ...

  3. ios kvo 要引入_腾讯社招iOS面试记录

    毕业好几年了,上周发送了简历给腾讯,参加了腾讯面试.具体部门这边就不说了.这次面试还是收获到了很多. 一面电话面试: 面试官主要是针对iOS相关的基础问题. 先简单自我介绍一下自己 对mrc和arc的 ...

  4. iOS面试中经常问的点 - RunTime

    一. RunTime简介 我将iOS的一些学习视频书籍资料总结在"码农Style"公众号里,需要的小伙伴可以自行获取 想要一起探讨学习iOS底层原理,架构的可以加我Q_233668 ...

  5. 分割view窗口不响应onmousewheel_什么是响应者链?ios面试攻克篇(三)

    '写在前面的话' 这些是我对iOS面试时会碰到的问题的解决方法, 整理出来分享给大家,有些错误不要 太好笑,希望对大家有所帮助. 大家有更好的解决办法也欢迎沟通交流. 一直以来都有写点儿东西的想法, ...

  6. [转] 上级向的十个iOS面试问题

    上级向的十个iOS面试问题 转自 http://onevcat.com/2013/04/ios-interview/ 不管对于招聘和应聘来说,面试都是很重要的一个环节,特别对于开发者来说,面试中的技术 ...

  7. 最新iOS面试必看题视频教程(附大神简历要素)

    2019独角兽企业重金招聘Python工程师标准>>> 本文是由尚学堂iOS学院总结的ios开发者在求职时会遇到的一些面试题 ,并通过实际代码演练将课程详尽的讲解出来,希望对学习io ...

  8. iOS 面试基础题目

    转载: iOS 面试基础题目 题目来自博客:面试百度的记录,有些问题我能回答一下,不能回答的或有更好的回答我放个相关链接供参考. 1面 Objective C runtime library:Obje ...

  9. iOS面试一般性问题

    转载自:http://www.huangyibiao.com/archives/391 注意:以下问题的参考答案均为笔者所答,不代表正确,问题答案因人而异,请根据自己的实际情况回答,若认为不合理,请在 ...

  10. iOS面试备战-网络篇

    计算机网络是计算机科学与技术专业的必修课,也是移动端,前端,后端都会涉及并用到的知识点,可想而知它的重要性.所以它也成为了iOS面试中经常被问及的问题.准备面试的话,网络相关的知识点一定不能错过.这里 ...

最新文章

  1. linux 查找 jdk 安装路径
  2. word中viso/math type公式比文字大
  3. mysql insert into select大量数据插入比较慢_史上最全MySQL锁机制
  4. SAP HANA解读-2012 SAP商业同略会分享
  5. 关于登录 token 的设计
  6. Java-用IDEA创建Java项目
  7. Asp.Net的性能问题
  8. linux计划任务与日志管理(日志分割/切割)
  9. 分类预测 | MATLAB实现ELM极限学习机多特征分类预测
  10. Django 文件下载
  11. WEB安全之代码安全----ESAPI
  12. 凯明启示录:倒闭风潮刚开始
  13. android 百度地图应用
  14. 2021正睿csp7连day3
  15. 打包docker镜像,推送远程服务器,部署到k8s步骤
  16. 《郑军的回忆》文章记录了我3年的痛苦过去
  17. p2p服务器的协议,P2P文件传输协议之BitTorrent协议
  18. ChatGPT与AIGC,新世界的创造者
  19. 期货反向跟单—我们与规律同在
  20. 根据词频、背景图绘制词云图

热门文章

  1. 英语diamaund钻石diamaund单词
  2. 为什么我要选择Java
  3. python 开发安卓 获取wifi_用python获取可用wifi信息(windows版)
  4. RHCSA考点-个人见解
  5. 江东子弟今犹在,不见霸王卷土来-------重启编程记录征程之序
  6. 前端框架 Vue 初探
  7. 产品经理——数据分析比较好用的工具和网站
  8. 【Mac终端】 root与普通用户切换(root/bash-3.2/sh-3.2/MacBook-Pro区别)
  9. 「镁客早报」特斯拉称已与中国多家银行达成协议获取建厂贷款;沃达丰将在英国19个城市推出5G服务...
  10. 零售药店计算机操作内容培训,零售药店的年度培训记录.docx