[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"similar-colourful987--bytedance-alibaba-interview":3,"tool-colourful987--bytedance-alibaba-interview":65},[4,23,32,40,49,57],{"id":5,"name":6,"github_repo":7,"description_zh":8,"stars":9,"difficulty_score":10,"last_commit_at":11,"category_tags":12,"status":22},2268,"ML-For-Beginners","microsoft\u002FML-For-Beginners","ML-For-Beginners 是由微软推出的一套系统化机器学习入门课程，旨在帮助零基础用户轻松掌握经典机器学习知识。这套课程将学习路径规划为 12 周，包含 26 节精炼课程和 52 道配套测验，内容涵盖从基础概念到实际应用的完整流程，有效解决了初学者面对庞大知识体系时无从下手、缺乏结构化指导的痛点。\n\n无论是希望转型的开发者、需要补充算法背景的研究人员，还是对人工智能充满好奇的普通爱好者，都能从中受益。课程不仅提供了清晰的理论讲解，还强调动手实践，让用户在循序渐进中建立扎实的技能基础。其独特的亮点在于强大的多语言支持，通过自动化机制提供了包括简体中文在内的 50 多种语言版本，极大地降低了全球不同背景用户的学习门槛。此外，项目采用开源协作模式，社区活跃且内容持续更新，确保学习者能获取前沿且准确的技术资讯。如果你正寻找一条清晰、友好且专业的机器学习入门之路，ML-For-Beginners 将是理想的起点。",85013,2,"2026-04-06T11:09:19",[13,14,15,16,17,18,19,20,21],"图像","数据工具","视频","插件","Agent","其他","语言模型","开发框架","音频","ready",{"id":24,"name":25,"github_repo":26,"description_zh":27,"stars":28,"difficulty_score":29,"last_commit_at":30,"category_tags":31,"status":22},3128,"ragflow","infiniflow\u002Fragflow","RAGFlow 是一款领先的开源检索增强生成（RAG）引擎，旨在为大语言模型构建更精准、可靠的上下文层。它巧妙地将前沿的 RAG 技术与智能体（Agent）能力相结合，不仅支持从各类文档中高效提取知识，还能让模型基于这些知识进行逻辑推理和任务执行。\n\n在大模型应用中，幻觉问题和知识滞后是常见痛点。RAGFlow 通过深度解析复杂文档结构（如表格、图表及混合排版），显著提升了信息检索的准确度，从而有效减少模型“胡编乱造”的现象，确保回答既有据可依又具备时效性。其内置的智能体机制更进一步，使系统不仅能回答问题，还能自主规划步骤解决复杂问题。\n\n这款工具特别适合开发者、企业技术团队以及 AI 研究人员使用。无论是希望快速搭建私有知识库问答系统，还是致力于探索大模型在垂直领域落地的创新者，都能从中受益。RAGFlow 提供了可视化的工作流编排界面和灵活的 API 接口，既降低了非算法背景用户的上手门槛，也满足了专业开发者对系统深度定制的需求。作为基于 Apache 2.0 协议开源的项目，它正成为连接通用大模型与行业专有知识之间的重要桥梁。",77062,3,"2026-04-04T04:44:48",[17,13,20,19,18],{"id":33,"name":34,"github_repo":35,"description_zh":36,"stars":37,"difficulty_score":29,"last_commit_at":38,"category_tags":39,"status":22},519,"PaddleOCR","PaddlePaddle\u002FPaddleOCR","PaddleOCR 是一款基于百度飞桨框架开发的高性能开源光学字符识别工具包。它的核心能力是将图片、PDF 等文档中的文字提取出来，转换成计算机可读取的结构化数据，让机器真正“看懂”图文内容。\n\n面对海量纸质或电子文档，PaddleOCR 解决了人工录入效率低、数字化成本高的问题。尤其在人工智能领域，它扮演着连接图像与大型语言模型（LLM）的桥梁角色，能将视觉信息直接转化为文本输入，助力智能问答、文档分析等应用场景落地。\n\nPaddleOCR 适合开发者、算法研究人员以及有文档自动化需求的普通用户。其技术优势十分明显：不仅支持全球 100 多种语言的识别，还能在 Windows、Linux、macOS 等多个系统上运行，并灵活适配 CPU、GPU、NPU 等各类硬件。作为一个轻量级且社区活跃的开源项目，PaddleOCR 既能满足快速集成的需求，也能支撑前沿的视觉语言研究，是处理文字识别任务的理想选择。",75054,"2026-04-07T10:38:03",[19,13,20,18],{"id":41,"name":42,"github_repo":43,"description_zh":44,"stars":45,"difficulty_score":46,"last_commit_at":47,"category_tags":48,"status":22},3215,"awesome-machine-learning","josephmisiti\u002Fawesome-machine-learning","awesome-machine-learning 是一份精心整理的机器学习资源清单，汇集了全球优秀的机器学习框架、库和软件工具。面对机器学习领域技术迭代快、资源分散且难以甄选的痛点，这份清单按编程语言（如 Python、C++、Go 等）和应用场景（如计算机视觉、自然语言处理、深度学习等）进行了系统化分类，帮助使用者快速定位高质量项目。\n\n它特别适合开发者、数据科学家及研究人员使用。无论是初学者寻找入门库，还是资深工程师对比不同语言的技术选型，都能从中获得极具价值的参考。此外，清单还延伸提供了免费书籍、在线课程、行业会议、技术博客及线下聚会等丰富资源，构建了从学习到实践的全链路支持体系。\n\n其独特亮点在于严格的维护标准：明确标记已停止维护或长期未更新的项目，确保推荐内容的时效性与可靠性。作为机器学习领域的“导航图”，awesome-machine-learning 以开源协作的方式持续更新，旨在降低技术探索门槛，让每一位从业者都能高效地站在巨人的肩膀上创新。",72149,1,"2026-04-03T21:50:24",[20,18],{"id":50,"name":51,"github_repo":52,"description_zh":53,"stars":54,"difficulty_score":46,"last_commit_at":55,"category_tags":56,"status":22},2234,"scikit-learn","scikit-learn\u002Fscikit-learn","scikit-learn 是一个基于 Python 构建的开源机器学习库，依托于 SciPy、NumPy 等科学计算生态，旨在让机器学习变得简单高效。它提供了一套统一且简洁的接口，涵盖了从数据预处理、特征工程到模型训练、评估及选择的全流程工具，内置了包括线性回归、支持向量机、随机森林、聚类等在内的丰富经典算法。\n\n对于希望快速验证想法或构建原型的数据科学家、研究人员以及 Python 开发者而言，scikit-learn 是不可或缺的基础设施。它有效解决了机器学习入门门槛高、算法实现复杂以及不同模型间调用方式不统一的痛点，让用户无需重复造轮子，只需几行代码即可调用成熟的算法解决分类、回归、聚类等实际问题。\n\n其核心技术亮点在于高度一致的 API 设计风格，所有估算器（Estimator）均遵循相同的调用逻辑，极大地降低了学习成本并提升了代码的可读性与可维护性。此外，它还提供了强大的模型选择与评估工具，如交叉验证和网格搜索，帮助用户系统地优化模型性能。作为一个由全球志愿者共同维护的成熟项目，scikit-learn 以其稳定性、详尽的文档和活跃的社区支持，成为连接理论学习与工业级应用的最",65679,"2026-04-07T11:38:52",[20,18,14],{"id":58,"name":59,"github_repo":60,"description_zh":61,"stars":62,"difficulty_score":10,"last_commit_at":63,"category_tags":64,"status":22},3364,"keras","keras-team\u002Fkeras","Keras 是一个专为人类设计的深度学习框架，旨在让构建和训练神经网络变得简单直观。它解决了开发者在不同深度学习后端之间切换困难、模型开发效率低以及难以兼顾调试便捷性与运行性能的痛点。\n\n无论是刚入门的学生、专注算法的研究人员，还是需要快速落地产品的工程师，都能通过 Keras 轻松上手。它支持计算机视觉、自然语言处理、音频分析及时间序列预测等多种任务。\n\nKeras 3 的核心亮点在于其独特的“多后端”架构。用户只需编写一套代码，即可灵活选择 TensorFlow、JAX、PyTorch 或 OpenVINO 作为底层运行引擎。这一特性不仅保留了 Keras 一贯的高层易用性，还允许开发者根据需求自由选择：利用 JAX 或 PyTorch 的即时执行模式进行高效调试，或切换至速度最快的后端以获得最高 350% 的性能提升。此外，Keras 具备强大的扩展能力，能无缝从本地笔记本电脑扩展至大规模 GPU 或 TPU 集群，是连接原型开发与生产部署的理想桥梁。",63927,"2026-04-04T15:24:37",[20,14,18],{"id":66,"github_repo":67,"name":68,"description_en":69,"description_zh":70,"ai_summary_zh":70,"readme_en":71,"readme_zh":72,"quickstart_zh":73,"use_case_zh":74,"hero_image_url":75,"owner_login":76,"owner_name":77,"owner_avatar_url":78,"owner_bio":79,"owner_company":80,"owner_location":81,"owner_email":82,"owner_twitter":83,"owner_website":83,"owner_url":84,"languages":85,"stars":90,"forks":91,"last_commit_at":92,"license":83,"difficulty_score":46,"env_os":93,"env_gpu":94,"env_ram":94,"env_deps":95,"category_tags":100,"github_topics":83,"view_count":10,"oss_zip_url":83,"oss_zip_packed_at":83,"status":22,"created_at":101,"updated_at":102,"faqs":103,"releases":119},5069,"colourful987\u002Fbytedance-alibaba-interview","bytedance-alibaba-interview","阿里、字节 一套高效的iOS面试题解答","bytedance-alibaba-interview 是一套专为 iOS 开发者打造的高效面试备战资料，内容源自互联网大厂（阿里巴巴、字节跳动）的真实面试题。它系统梳理了 Objective-C 运行时（Runtime）的核心机制，深入解析了内存模型、元类设计、属性与实例变量的区别、类结构（class_rw_t 与 class_ro_t）以及分类（Category）的加载顺序等高频难点。\n\n这套资料不仅提供了清晰的理论解答，还附带了可调试运行的 objc-runtime 源码注释，帮助读者从底层原理出发，彻底理解消息发送、对象存储等关键机制，从而解决面试中“只知表面、不懂底层”的痛点。通过结合代码实战与理论分析，它能有效提升开发者对 iOS 核心技术的掌握深度。\n\n该资源非常适合准备一线互联网公司技术面试的中高级 iOS 工程师，也适合希望深入研读 Runtime 源码的技术爱好者。其独特亮点在于将抽象的运行时概念与具体的源码实现一一对应，并明确了内推渠道，兼具学习价值与求职实用性。","# 阿里、字节 一套高效的iOS面试题解答\n> - 面试题出自掘金的一篇文章[《阿里、字节：一套高效的iOS面试题》](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=https%3A%2F%2Fjuejin.im%2Fpost%2F5e397ccaf265da570b3f1b02)\n> - 欢迎转载，转载请注明出处：[pmst-swiftgg](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=%5Bhttps%3A%2F%2Fwww.jianshu.com%2Fp%2Fc1765a6305ab%5D(https%3A%2F%2Fwww.jianshu.com%2Fp%2Fc1765a6305ab))\n> - 调试好可运行的源码 [objc-runtime](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=https%3A%2F%2Fgithub.com%2Fcolourful987%2F2020-Read-Record%2Ftree%2Fmaster%2FAnnotated%20source%20code%2Fobjc4-750)，官网找 [objc4](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=https%3A%2F%2Fopensource.apple.com%2Ftarballs%2Fobjc4%2F)\n> - 关于内推：目前博主就职于字节跳动，可帮忙内推，关于面试也可解答一二，欢迎发邮件联系或投递简历，我会第一时间处理，Email: mcxcode@163.com\n> - 最后修订：2020\u002F05\u002F11\n\n[TOC]\n\n# 一、runtime相关问题\n\n> * 调试好可运行的源码 [objc-runtime](https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record\u002Ftree\u002Fmaster\u002FAnnotated%20source%20code\u002Fobjc4-750)，官网找 [objc4](https:\u002F\u002Fopensource.apple.com\u002Ftarballs\u002Fobjc4\u002F)；\n>\n\n## 结构模型\n\n### 1. 介绍下runtime的内存模型（isa、对象、类、metaclass、结构体的存储信息等）\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_1d68fef9a20e.png)\n\n\n\n### 2. 为什么要设计metaclass\n\n* 类对象、元类对象能够复用消息发送流程机制；\n* 单一职责原则\n\n### 3. `class_copyIvarList` & `class_copyPropertyList`区别\n\n`property` 正常使用会生成对应的实例变量，所以 `Ivar` 可以查到。\n\n`class_copyIvarList` 获取类对象中的所有实例变量信息，从 `class_ro_t` 中获取：\n\n```objective-c\nIvar *\nclass_copyIvarList(Class cls, unsigned int *outCount)\n{\n    const ivar_list_t *ivars;\n    Ivar *result = nil;\n    unsigned int count = 0;\n\n    if (!cls) {\n        if (outCount) *outCount = 0;\n        return nil;\n    }\n\n    mutex_locker_t lock(runtimeLock);\n\n    assert(cls->isRealized());\n    \n    if ((ivars = cls->data()->ro->ivars)  &&  ivars->count) {\n        result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar));\n        \n        for (auto& ivar : *ivars) {\n            if (!ivar.offset) continue;  \u002F\u002F anonymous bitfield\n            result[count++] = &ivar;\n        }\n        result[count] = nil;\n    }\n    \n    if (outCount) *outCount = count;\n    return result;\n}\n```\n\n`class_copyPropertyList` 获取类对象中的属性信息， `class_rw_t` 的 `properties`，先后输出了 category \u002F extension\u002F baseClass 的属性，而且仅输出当前的类的属性信息，而不会向上去找 superClass 中定义的属性。\n\n```objective-c\nobjc_property_t *\nclass_copyPropertyList(Class cls, unsigned int *outCount)\n{\n    if (!cls) {\n        if (outCount) *outCount = 0;\n        return nil;\n    }\n\n    mutex_locker_t lock(runtimeLock);\n\n    checkIsKnownClass(cls);\n    assert(cls->isRealized());\n    \n    auto rw = cls->data();\n\n    property_t **result = nil;\n    unsigned int count = rw->properties.count();\n    if (count > 0) {\n        result = (property_t **)malloc((count + 1) * sizeof(property_t *));\n\n        count = 0;\n        for (auto& prop : rw->properties) {\n            result[count++] = &prop;\n        }\n        result[count] = nil;\n    }\n\n    if (outCount) *outCount = count;\n    return (objc_property_t *)result;\n}\n```\n\n> Q1: `class_ro_t` 中的 `baseProperties` 呢？\n>\n> Q2: `class_rw_t` 中的 `properties` 包含了所有属性，那何时注入进去的呢？ 答案见 5.\n\n### 4. `class_rw_t` 和 `class_ro_t` 的区别\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_8b32d7ebec51.png)\n\n测试发现，`class_rw_t` 中的 `properties` 属性按顺序包含分类\u002F扩展\u002F基类中的属性。\n\n```objective-c\nstruct class_ro_t {\n    uint32_t flags;\n    uint32_t instanceStart;\n    uint32_t instanceSize;\n#ifdef __LP64__\n    uint32_t reserved;\n#endif\n\n    const uint8_t * ivarLayout;\n    \n    const char * name;\n    method_list_t * baseMethodList;\n    protocol_list_t * baseProtocols;\n    const ivar_list_t * ivars;\n\n    const uint8_t * weakIvarLayout;\n    property_list_t *baseProperties;\n\n    method_list_t *baseMethods() const {\n        return baseMethodList;\n    }\n};\n\nstruct class_rw_t {\n    \u002F\u002F Be warned that Symbolication knows the layout of this structure.\n    uint32_t flags;\n    uint32_t version;\n\n    const class_ro_t *ro;\n\n    method_array_t methods;\n    property_array_t properties;\n    protocol_array_t protocols;\n\n    Class firstSubclass;\n    Class nextSiblingClass;\n\n    char *demangledName;\n\n#if SUPPORT_INDEXED_ISA\n    uint32_t index;\n#endif\n}\n```\n\n\n\n### 5. `category`如何被加载的,两个category的`load`方法的加载顺序，两个category的同名方法的加载顺序\n\n> `+load` 方法是 images 加载的时候调用，假设有一个 Person 类，其主类和所有分类的 `+load` 都会被调用，优先级是先调用主类，且如果主类有继承链，那么加载顺序还必须是基类的 `+load` ，接着是父类，最后是子类；category 的 `+load` 则是按照编译顺序来的，先编译的先调用，后编译的后调用，可在 Xcode  的 BuildPhase 中查看，[测试 Demo 可点击下载运行](.\u002Fdemos\u002FTestLoad方法)；\n>\n> 另外一个问题是 `initialize` 的加载顺序，其实是类第一次被使用到的时候会被调用，底层实现有个逻辑先判断父类是否被初始化过，没有则先调用父类，然后在调用当前类的 `initialize` 方法；试想一种情况，一个类 A 存在多个 category ，且 category中各自实现了 initialize 方法，这时候走的是 **消息发送流程**，也就说 initialize 方法只会调用一次，也就是最后编译的那个category中的 initialize 方法，验证demo见上；\n>\n> 再考虑一种情况：如果`+load` 方法中调用了其他类：比如 B 的某个方法，其实说白了就是走消息发送流程，由于 B 没有初始化过，则会调用其 initialize 方法，但此刻 B 的 +load 方法可能还没有被系统调用过。\n>\n> **小结：** 不管是 load 还是 initialize 方法都是 runtime 底层自动调用的，如果开发自己手动进行了 `[super load]` 或者 `[super initialize]` 方法，实际上是走消息发送流程，那么这里也涉及了一个调用流程，需要引起注意。\n\n`... -> realizeClass -> methodizeClass(用于Attach categories)-> attachCategories` 关键就是在 methodizeClass 方法实现中\n\n```objective-c\nstatic void methodizeClass(Class cls)\n{\n    runtimeLock.assertLocked();\n\n    bool isMeta = cls->isMetaClass();\n    auto rw = cls->data();\n    auto ro = rw->ro;\n  \t\n  \t\u002F\u002F =======================================\n\t\t\u002F\u002F 省略.....\n  \t\u002F\u002F =======================================\n  \n    property_list_t *proplist = ro->baseProperties;\n    if (proplist) {\n        rw->properties.attachLists(&proplist, 1);\n    }\n\n  \t\u002F\u002F =======================================\n\t\t\u002F\u002F 省略.....\n  \t\u002F\u002F =======================================\n\n    \u002F\u002F Attach categories.\n    category_list *cats = unattachedCategoriesForClass(cls, true \u002F*realizing*\u002F);\n    attachCategories(cls, cats, false \u002F*don't flush caches*\u002F);\n\n  \t\u002F\u002F =======================================\n\t\t\u002F\u002F 省略.....\n  \t\u002F\u002F =======================================\n    \n    if (cats) free(cats);\n\n}\n```\n\n上面代码能确定 baseProperties 在前，category 在后，但决定顺序的是 `rw->properties.attachLists` 这个方法：\n\n```objective-c\nproperty_list_t *proplist = ro->baseProperties;\nif (proplist) {\n  rw->properties.attachLists(&proplist, 1);\n}\n\n\u002F\u002F\u002F category 被附加进去\nvoid attachLists(List* const * addedLists, uint32_t addedCount) {\n        if (addedCount == 0) return;\n\n        if (hasArray()) {\n            \u002F\u002F many lists -> many lists\n            uint32_t oldCount = array()->count;\n            uint32_t newCount = oldCount + addedCount;\n            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));\n            array()->count = newCount;\n            \n            \u002F\u002F 将旧内容移动偏移量 addedCount 然后将 addedLists copy 到起始位置\n          \t\u002F*\n          \t\tstruct array_t {\n        \t\t\t\tuint32_t count;\n        \t\t\t\tList* lists[0];\n    \t\t\t\t\t};\n          \t*\u002F\n            memmove(array()->lists + addedCount, array()->lists, \n                    oldCount * sizeof(array()->lists[0]));\n            memcpy(array()->lists, addedLists, \n                   addedCount * sizeof(array()->lists[0]));\n        }\n        else if (!list  &&  addedCount == 1) {\n            \u002F\u002F 0 lists -> 1 list\n            list = addedLists[0];\n        } \n        else {\n            \u002F\u002F 1 list -> many lists\n            List* oldList = list;\n            uint32_t oldCount = oldList ? 1 : 0;\n            uint32_t newCount = oldCount + addedCount;\n            setArray((array_t *)malloc(array_t::byteSize(newCount)));\n            array()->count = newCount;\n            if (oldList) array()->lists[addedCount] = oldList;\n            memcpy(array()->lists, addedLists, \n                   addedCount * sizeof(array()->lists[0]));\n        }\n    }\n```\n\n所以 category 的属性总是在前面的，baseClass的属性被往后偏移了。\n\n>  Q1：那么多个 category 的顺序呢？答案见6\n\n2020\u002F03\u002F18 补充下应用程序 image 镜像加载到内存中时， Category 解析的过程，注意下面的 `while(i--)` 这里倒叙将 category 中的协议 方法 属性添加到了 `rw = cls->data()` 中的 `methods\u002Fproperties\u002Fprotocols` 中。\n\n```objective-c\nstatic void \nattachCategories(Class cls, category_list *cats, bool flush_caches)\n{\n    if (!cats) return;\n    if (PrintReplacedMethods) printReplacements(cls, cats);\n\n    bool isMeta = cls->isMetaClass();\n\n    \u002F\u002F fixme rearrange to remove these intermediate allocations\n    method_list_t **mlists = (method_list_t **)\n        malloc(cats->count * sizeof(*mlists));\n    property_list_t **proplists = (property_list_t **)\n        malloc(cats->count * sizeof(*proplists));\n    protocol_list_t **protolists = (protocol_list_t **)\n        malloc(cats->count * sizeof(*protolists));\n\n    \u002F\u002F Count backwards through cats to get newest categories first\n    int mcount = 0;\n    int propcount = 0;\n    int protocount = 0;\n    int i = cats->count;\n    bool fromBundle = NO;\n    while (i--) {\n        auto& entry = cats->list[i];\n\n        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);\n        if (mlist) {\n            mlists[mcount++] = mlist;\n            fromBundle |= entry.hi->isBundle();\n        }\n\n        property_list_t *proplist = \n            entry.cat->propertiesForMeta(isMeta, entry.hi);\n        if (proplist) {\n            proplists[propcount++] = proplist;\n        }\n\n        protocol_list_t *protolist = entry.cat->protocols;\n        if (protolist) {\n            protolists[protocount++] = protolist;\n        }\n    }\n    auto rw = cls->data();\n\t\t\n  \t\u002F\u002F 注意下面的代码，上面采用倒叙遍历方式，所以后编译的 category 会先add到数组的前部\n    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);\n    rw->methods.attachLists(mlists, mcount);\n    free(mlists);\n    if (flush_caches  &&  mcount > 0) flushCaches(cls);\n\n    rw->properties.attachLists(proplists, propcount);\n    free(proplists);\n\n    rw->protocols.attachLists(protolists, protocount);\n    free(protolists);\n}\n```\n\n\n\n### 6. `category` & `extension`区别，能给NSObject添加Extension吗，结果如何\n\ncategory:\n\n* 运行时添加分类属性\u002F协议\u002F方法\n* 分类添加的方法会“覆盖”原类方法，因为方法查找的话是从头至尾，一旦查找到了就停止了\n* 同名分类方法谁生效取决于编译顺序，image 读取的信息是倒叙的，所以编译越靠后的越先读入\n* 名字相同的分类会引起编译报错；\n\nextension:\n\n* 编译时决议\n* 只以声明的形式存在，多数情况下就存在于 .m 文件中；\n* 不能为系统类添加扩展\n\n### 7. 消息转发机制，消息转发机制和其他语言的消息机制优劣对比\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_fca37514705f.png)\n\n### 8. 在方法调用的时候，`方法查询-> 动态解析-> 消息转发` 之前做了什么\n\nOC中的方法调用，编译后的代码最终都会转成 `objc_msgSend(id , SEL, ...)` 方法进行调用，这个方法第一个参数是一个消息接收者对象，runtime通过这个对象的isa指针找到这个对象的类对象，从类对象中的cache中查找(**哈希查找，bucket 桶实现**)是否存在SEL对应的IMP，若不存在，则会在 method_list中查找（二分查找或者顺序查找），如果还是没找到，则会到supper_class中查找，仍然没找到的话，就会调用_objc_msgForward(id, SEL, ...)进行消息转发。\n\n### 9. `IMP`、`SEL`、`Method`的区别和使用场景\n\n三者的定义：\n\n```objective-c\ntypedef struct method_t *Method;\n\nusing MethodListIMP = IMP;\n\nstruct method_t {\n    SEL name;\n    const char *types;\n    MethodListIMP imp;\n};\n```\n\nMethod 同样是个对象，封装了方法名和实现，关于 [Type Encodings](https:\u002F\u002Fdeveloper.apple.com\u002Flibrary\u002Farchive\u002Fdocumentation\u002FCocoa\u002FConceptual\u002FObjCRuntimeGuide\u002FArticles\u002FocrtTypeEncodings.html#\u002F\u002Fapple_ref\u002Fdoc\u002Fuid\u002FTP40008048-CH100-SW1)。\n\n| Code               | Meaning                                                      |\n| :----------------- | :----------------------------------------------------------- |\n| `c`                | A `char`                                                     |\n| `i`                | An `int`                                                     |\n| `s`                | A `short`                                                    |\n| `l`                | A `long l` is treated as a 32-bit quantity on 64-bit programs. |\n| `q`                | A `long long`                                                |\n| `C`                | An `unsigned char`                                           |\n| `I`                | An `unsigned int`                                            |\n| `S`                | An `unsigned short`                                          |\n| `L`                | An `unsigned long`                                           |\n| `Q`                | An `unsigned long long`                                      |\n| `f`                | A `float`                                                    |\n| `d`                | A `double`                                                   |\n| `B`                | A C++ `bool` or a C99 `_Bool`                                |\n| `v`                | A `void`                                                     |\n| `*`                | A character string (`char *`)                                |\n| `@`                | An object (whether statically typed or typed `id`)           |\n| `#`                | A class object (`Class`)                                     |\n| `:`                | A method selector (`SEL`)                                    |\n| [*array type*]     | An array                                                     |\n| {*name=type...*}   | A structure                                                  |\n| (*name*=*type...*) | A union                                                      |\n| `b`num             | A bit field of *num* bits                                    |\n| `^`type            | A pointer to *type*                                          |\n| `?`                | An unknown type (among other things, this code is used for function pointers) |\n\n `-(void)hello:(NSString *)name` encode 下就是 `v@:@`。\n\n### 10. `load`、`initialize`方法的区别什么？在继承关系中他们有什么区别\n\nload 方法调用时机，而且只调用当前类本身，不会调用superClass 的 `+load` 方法：\n\n```objective-c\nvoid\nload_images(const char *path __unused, const struct mach_header *mh)\n{\n    \u002F\u002F Return without taking locks if there are no +load methods here.\n    if (!hasLoadMethods((const headerType *)mh)) return;\n\n    recursive_mutex_locker_t lock(loadMethodLock);\n\n    \u002F\u002F Discover load methods\n    {\n        mutex_locker_t lock2(runtimeLock);\n        prepare_load_methods((const headerType *)mh);\n    }\n\n    \u002F\u002F Call +load methods (without runtimeLock - re-entrant)\n    call_load_methods();\n}\n\nvoid call_load_methods(void)\n{\n    static bool loading = NO;\n    bool more_categories;\n\n    loadMethodLock.assertLocked();\n\n    \u002F\u002F Re-entrant calls do nothing; the outermost call will finish the job.\n    if (loading) return;\n    loading = YES;\n\n    void *pool = objc_autoreleasePoolPush();\n\n    do {\n        \u002F\u002F 1. Repeatedly call class +loads until there aren't any more\n        while (loadable_classes_used > 0) {\n            call_class_loads();\n        }\n\n        \u002F\u002F 2. Call category +loads ONCE\n        more_categories = call_category_loads();\n\n        \u002F\u002F 3. Run more +loads if there are classes OR more untried categories\n    } while (loadable_classes_used > 0  ||  more_categories);\n\n    objc_autoreleasePoolPop(pool);\n\n    loading = NO;\n}\n```\n\n`+initialize` 实现\n\n```objective-c\nvoid _class_initialize(Class cls)\n{\n    assert(!cls->isMetaClass());\n\n    Class supercls;\n    bool reallyInitialize = NO;\n\n    \u002F\u002F Make sure super is done initializing BEFORE beginning to initialize cls.\n    \u002F\u002F See note about deadlock above.\n    supercls = cls->superclass;\n    if (supercls  &&  !supercls->isInitialized()) {\n        _class_initialize(supercls);\n    }\n    \n    \u002F\u002F Try to atomically set CLS_INITIALIZING.\n    {\n        monitor_locker_t lock(classInitLock);\n        if (!cls->isInitialized() && !cls->isInitializing()) {\n            cls->setInitializing();\n            reallyInitialize = YES;\n        }\n    }\n    \n    if (reallyInitialize) {\n        \u002F\u002F We successfully set the CLS_INITIALIZING bit. Initialize the class.\n        \n        \u002F\u002F Record that we're initializing this class so we can message it.\n        _setThisThreadIsInitializingClass(cls);\n\n        if (MultithreadedForkChild) {\n            \u002F\u002F LOL JK we don't really call +initialize methods after fork().\n            performForkChildInitialize(cls, supercls);\n            return;\n        }\n        \n        \u002F\u002F Send the +initialize message.\n        \u002F\u002F Note that +initialize is sent to the superclass (again) if \n        \u002F\u002F this class doesn't implement +initialize. 2157218\n        if (PrintInitializing) {\n            _objc_inform(\"INITIALIZE: thread %p: calling +[%s initialize]\",\n                         pthread_self(), cls->nameForLogging());\n        }\n\n        \u002F\u002F Exceptions: A +initialize call that throws an exception \n        \u002F\u002F is deemed to be a complete and successful +initialize.\n        \u002F\u002F\n        \u002F\u002F Only __OBJC2__ adds these handlers. !__OBJC2__ has a\n        \u002F\u002F bootstrapping problem of this versus CF's call to\n        \u002F\u002F objc_exception_set_functions().\n#if __OBJC2__\n        @try\n#endif\n        {\n            callInitialize(cls);\n\n            if (PrintInitializing) {\n                _objc_inform(\"INITIALIZE: thread %p: finished +[%s initialize]\",\n                             pthread_self(), cls->nameForLogging());\n            }\n        }\n#if __OBJC2__\n        @catch (...) {\n            if (PrintInitializing) {\n                _objc_inform(\"INITIALIZE: thread %p: +[%s initialize] \"\n                             \"threw an exception\",\n                             pthread_self(), cls->nameForLogging());\n            }\n            @throw;\n        }\n        @finally\n#endif\n        {\n            \u002F\u002F Done initializing.\n            lockAndFinishInitializing(cls, supercls);\n        }\n        return;\n    }\n    \n    else if (cls->isInitializing()) {\n        \u002F\u002F We couldn't set INITIALIZING because INITIALIZING was already set.\n        \u002F\u002F If this thread set it earlier, continue normally.\n        \u002F\u002F If some other thread set it, block until initialize is done.\n        \u002F\u002F It's ok if INITIALIZING changes to INITIALIZED while we're here, \n        \u002F\u002F   because we safely check for INITIALIZED inside the lock \n        \u002F\u002F   before blocking.\n        if (_thisThreadIsInitializingClass(cls)) {\n            return;\n        } else if (!MultithreadedForkChild) {\n            waitForInitializeToComplete(cls);\n            return;\n        } else {\n            \u002F\u002F We're on the child side of fork(), facing a class that\n            \u002F\u002F was initializing by some other thread when fork() was called.\n            _setThisThreadIsInitializingClass(cls);\n            performForkChildInitialize(cls, supercls);\n        }\n    }\n    \n    else if (cls->isInitialized()) {\n        \u002F\u002F Set CLS_INITIALIZING failed because someone else already \n        \u002F\u002F   initialized the class. Continue normally.\n        \u002F\u002F NOTE this check must come AFTER the ISINITIALIZING case.\n        \u002F\u002F Otherwise: Another thread is initializing this class. ISINITIALIZED \n        \u002F\u002F   is false. Skip this clause. Then the other thread finishes \n        \u002F\u002F   initialization and sets INITIALIZING=no and INITIALIZED=yes. \n        \u002F\u002F   Skip the ISINITIALIZING clause. Die horribly.\n        return;\n    }\n    \n    else {\n        \u002F\u002F We shouldn't be here. \n        _objc_fatal(\"thread-safe class init in objc runtime is buggy!\");\n    }\n}\n\nvoid callInitialize(Class cls)\n{\n    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);\n    asm(\"\");\n}\n```\n\n注意看上面的调用了 ` callInitialize(cls)` 然后又调用了 `lockAndFinishInitializing(cls, supercls)`。 \n\n> 摘自[iOS App冷启动治理](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c0a17d6e51d4570cf60d102?utm_source=gold_browser_extension) 一文中对 Dyld 在各阶段所做的事情：\n\n| 阶段         | 工作                                                         |\n| ------------ | ------------------------------------------------------------ |\n| 加载动态库   | Dyld从主执行文件的header获取到需要加载的所依赖动态库列表，然后它需要找到每个 dylib，而应用所依赖的 dylib 文件可能会再依赖其他 dylib，所以所需要加载的是动态库列表一个递归依赖的集合 |\n| Rebase和Bind | - Rebase在Image内部调整指针的指向。在过去，会把动态库加载到指定地址，所有指针和数据对于代码都是对的，而现在地址空间布局是随机化，所以需要在原来的地址根据随机的偏移量做一下修正 - Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定，dyld需要去符号表里查找，找到symbol对应的实现 |\n| Objc setup   | - 注册Objc类 (class registration) - 把category的定义插入方法列表 (category registration) - 保证每一个selector唯一 (selector uniqing) |\n| Initializers | - Objc的+load()函数 - C++的构造函数属性函数 - 非基本类型的C++静态全局变量的创建(通常是类或结构体) |\n\n最后 dyld 会调用 main() 函数，main() 会调用 UIApplicationMain()，before main()的过程也就此完成。\n\n### 11. 说说消息转发机制的优劣\n\n## 内存管理\n\n### 1.`weak`的实现原理？`SideTable`的结构是什么样的\n\n> 解答参考自瓜神的[ weak 弱引用的实现方式](https:\u002F\u002Fwww.desgard.com\u002FiOS-Source-Probe\u002FObjective-C\u002FRuntime\u002Fweak%20弱引用的实现方式.html) 。\n\n```objective-c\nNSObject *p = [[NSObject alloc] init];\n__weak NSObject *p1 = p;\n\u002F\u002F ====> 底层是runtime的 objc_initWeak\n\u002F\u002F xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.2 main.m 得不到下面的代码，还是说命令参数不对。\nNSObject objc_initWeak(&p, 对象指针);\n```\n\n通过 runtime 源码可以看到 `objc_initWeak` 实现：\n\n```objective-c\nid\nobjc_initWeakOrNil(id *location, id newObj)\n{\n    if (!newObj) {\n        *location = nil;\n        return nil;\n    }\n\n    return storeWeak\u003CDontHaveOld, DoHaveNew, DontCrashIfDeallocating>\n        (location, (objc_object*)newObj);\n}\n```\nSideTable 结构体在 runtime 底层用于引用计数和弱引用关联表，其数据结构是这样：\n\n```c++\nstruct SideTable {\n    \u002F\u002F 自旋锁\n    spinlock_t slock;\n    \u002F\u002F 引用计数\n    RefcountMap refcnts;\n    \u002F\u002F weak 引用\n    weak_table_t weak_table;\n}\n\nstruct weak_table_t {\n    \u002F\u002F 保存了所有指向指定对象的 weak 指针\n    weak_entry_t *weak_entries;\n    \u002F\u002F 存储空间\n    size_t    num_entries;\n    \u002F\u002F 参与判断引用计数辅助量\n    uintptr_t mask;\n    \u002F\u002F hash key 最大偏移值\n    uintptr_t max_hash_displacement;\n};\n```\n\n根据对象的地址在缓存中取出对应的 `SideTable` 实例，\n\n> key 就是对象指针进行哈希算法后的值，全局的 SideTables 表个数根据平台分配了 8 或 64个，因此必定存在多个对象共用同一个 SideTable 的情况，不过SideTable中的 `weak_table_t` 和 `RefcountMap` 又是个哈希表，此时的 key 是对指针进行取反操作，另外还做了哈希碰撞处理。\n\n```c++\n\u002F\u002F objc-private.h\n#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR\n    enum { StripeCount = 8 };\n#else\n    enum { StripeCount = 64 };\n#endif\n\nstatic unsigned int indexForPointer(const void *p) {\n  uintptr_t addr = reinterpret_cast\u003Cuintptr_t>(p);\n  return ((addr >> 4) ^ (addr >> 9)) % StripeCount;\n}\n```\n\n```c++\nstatic SideTable *tableForPointer(const void *p)\n```\n\n或者如上面源码中 ` &SideTables()[newObj]` 方式取表，**这里的 newObj 是实例对象用其指针作为 key 拿到 从全局的 SideTables 中拿到实例自身对应的那张 SideTable**。\n\n```\nstatic StripedMap\u003CSideTable>& SideTables() {\n    return *reinterpret_cast\u003CStripedMap\u003CSideTable>*>(SideTableBuf);\n}\n```\n\n取出实例方法的实现中，使用了 C++ 标准转换运算符 **reinterpret_cast** ，其表达方式为：\n\n```c++\nreinterpret_cast \u003Cnew_type> (expression)\n```\n\n指向某个对象A的所有  weak 关键字修饰的引用都 append 到 `weak_entry_t` 结构体中的`referrers`， 同时`weak_entry_t` 的 `referent` 就是对象A，之后在dealloc 释放时遍历 `weak_table` 遍历时会判断 referent 是否为对象 A 取到 `weak_entry_t`，加入到该 SideTable 中 `weak_table` 中：\n\n> weak 底层实现其实不难，不同架构下 SideTable 表数量也是不同的（8和64）,所以用对象指针作为Key，存在键值冲突的问题，因此在设计上也要解决该问题，源码有体现。\n\n```c++\ntypedef objc_object ** weak_referrer_t;\n\nstruct weak_entry_t {\n    DisguisedPtr\u003Cobjc_object> referent;\n    union {\n        struct {\n            weak_referrer_t *referrers;\n            uintptr_t        out_of_line : 1;\n            uintptr_t        num_refs : PTR_MINUS_1;\n            uintptr_t        mask;\n            uintptr_t        max_hash_displacement;\n        };\n        struct {\n            \u002F\u002F out_of_line=0 is LSB of one of these (don't care which)\n            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];\n        };\n }\n```\n\n旧对象解除注册操作 `weak_unregister_no_lock` 和 新对象添加注册操作 `weak_register_no_lock` ，具体实现可前往 runtime 源码中查看或查看瓜的博文。\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_06eb45bb366b.png)\n\n`weak` 关键字修饰的对象有两种情况：栈上和堆上。上图主要解释 `id referent_id 和 id *referrer_id`，\n\n* 如果是栈上， `referrer` 值为 0x77889900，`referent` 值为 0x11223344\n* 如果是堆上 ， `referrer`  值为 0x1100000+ offset（也就是 weak a 所在堆上的地址），`referent` 值为 0x11223344。\n\n> 如此现在类 A 的实例对象有两个 weak 变量指向它，一个在堆上，一个在栈上。\n\n```c++\nvoid\nweak_unregister_no_lock(weak_table_t *weak_table, id referent_id, \n                        id *referrer_id)\n{\n    objc_object *referent = (objc_object *)referent_id;   \u002F\u002F  0x11223344\n    objc_object **referrer = (objc_object **)referrer_id; \u002F\u002F  0x77889900\n\n    weak_entry_t *entry;\n\n    if (!referent) return;\n\t\t\n  \t\u002F\u002F 从 weak_table 中找到 referent 也就是上面类A的实例对象\n    if ((entry = weak_entry_for_referent(weak_table, referent))) {\n      \t\u002F\u002F 在 entry 结构体中的 referrers 数组中找到指针 referrer 所在位置\n      \t\u002F\u002F 将原本存储 referrer 值的位置置为 nil，相当于做了一个解绑操作\n      \t\u002F\u002F 因为 referrer 要和其他对象建立关系了\n        remove_referrer(entry, referrer);\n        bool empty = true;\n        if (entry->out_of_line()  &&  entry->num_refs != 0) {\n            empty = false;\n        }\n        else {\n            for (size_t i = 0; i \u003C WEAK_INLINE_COUNT; i++) {\n                if (entry->inline_referrers[i]) {\n                    empty = false; \n                    break;\n                }\n            }\n        }\n\n        if (empty) {\n            weak_entry_remove(weak_table, entry);\n        }\n    }\n\n    \u002F\u002F Do not set *referrer = nil. objc_storeWeak() requires that the \n    \u002F\u002F value not change.\n}\n```\n\n> weak 关键字修饰的属性或者变量为什么在对应类实例dealloc后会置为nil，那是因为在类实例释放的时候，dealloc 会从全局的引用计数和weak计数表sideTable**s**中，通过实例地址去找到属于自己的那张表，表中的 weak_table->weak_entries 存储了所有 entry 对象——其实就是所有指向这个实例对象的变量，`weak_entry_t` 中的 `referrers` 数组存储的就是变量或属性的内存地址，逐一置为nil即可。\n\n\n\n### 2. 关联对象的应用？系统如何实现关联对象的\n\n关联对象基本使用方法：\n\n```objective-c\n#import \u003Cobjc\u002Fruntime.h>\n\nstatic NSString * const kKeyOfImageProperty;\n\n@implementation UIView (Image)\n\n- (UIImage *)pt_image {\n    return objc_getAssociatedObject(self, &kKeyOfImageProperty);\n}\n\n- (void)setPTImage:(UIImage *)image {\n    objc_setAssociatedObject(self, &kKeyOfImageProperty, image,OBJC_ASSOCIATION_RETAIN);\n}\n@end\n```\n\n`objc_AssociationPolicy` 关联对象持有策略有如下几种 ：\n\n| Behavior                            | @property Equivalent                                | Description                                    |\n| ----------------------------------- | --------------------------------------------------- | ---------------------------------------------- |\n| OBJC_ASSOCIATION_ASSIGN             | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。                     |\n| OBJC_ASSOCIATION_RETAIN_NONATOMIC   | @property (nonatomic, strong)                       | 指定一个关联对象的强引用，不能被原子化使用。   |\n| OBJC_ASSOCIATION_COPY_NONATOMIC     | @property (nonatomic, copy)                         | 指定一个关联对象的copy引用，不能被原子化使用。 |\n| OBJC_ASSOCIATION_RETAIN             | @property (atomic, strong)                          | 指定一个关联对象的强引用，能被原子化使用。     |\n| OBJC_ASSOCIATION_COPY               | @property (atomic, copy)                            | 指定一个关联对象的copy引用，能被原子化使用。   |\n| OBJC_ASSOCIATION_GETTER_AUTORELEASE |                                                     | 自动释放类型                                   |\n\n> 摘自[瓜地](https:\u002F\u002Fwww.desgard.com\u002FiOS-Source-Probe\u002FObjective-C\u002FRuntime\u002F浅谈Associated%20Objects.html)：OBJC_ASSOCIATION_ASSIGN类型的关联对象和`weak`有一定差别，而更加接近于`unsafe_unretained`，即当目标对象遭到摧毁时，属性值不会自动清空。（翻译自[Associated Objects](http:\u002F\u002Fnshipster.com\u002Fassociated-objects\u002F)）\n>\n> 同样是[Associated Objects](http:\u002F\u002Fnshipster.com\u002Fassociated-objects\u002F)文中，总结了三个关于Associated Objects用法：\n\n> - **为Class添加私有成员**：例如在AFNetworking中，[在UIImageView里添加了**imageRequestOperation**对象](https:\u002F\u002Fgithub.com\u002FAFNetworking\u002FAFNetworking\u002Fblob\u002F2.1.0\u002FUIKit%2BAFNetworking\u002FUIImageView%2BAFNetworking.m#L57-L63)，从而保证了异步加载图片。\n> - **为Class添加共有成员**：例如在FDTemplateLayoutCell中，使用Associated Objects来缓存每个cell的高度（[代码片段1](https:\u002F\u002Fgithub.com\u002Fmconintet\u002FUITableView-FDTemplateLayoutCell\u002Fblob\u002Fmaster\u002FClasses\u002FUITableView+FDIndexPathHeightCache.m#L124)、[代码片段2](https:\u002F\u002Fgithub.com\u002Fmconintet\u002FUITableView-FDTemplateLayoutCell\u002Fblob\u002Fmaster\u002FClasses\u002FUITableView+FDKeyedHeightCache.m#L81)）。通过分配不同的key，在复用cell的时候即时取出，增加效率。\n> - **创建KVO对象**：建议使用category来创建关联对象作为观察者。可以参考[*Objective-C Associated Objects*](http:\u002F\u002Fkingscocoa.com\u002Ftutorials\u002Fassociated-objects\u002F)这篇文的例子。\n\n源码实现非常简单，我添加了完整注释，对c++语法也做了一定解释：\n\n```c++\nid _object_get_associative_reference(id object, void *key) {\n    id value = nil;\n    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;\n    {\n        AssociationsManager manager;\n        \u002F\u002F manager.associations() 返回的是一个 `AssociationsHashMap` 对象(*_map)\n        \u002F\u002F 所以这里 `&associations` 中用了 `&`\n        AssociationsHashMap &associations(manager.associations());\n        \u002F\u002F intptr_t 是为了兼容平台，在64位的机器上，intptr_t和uintptr_t分别是long int、unsigned long int的别名；在32位的机器上，intptr_t和uintptr_t分别是int、unsigned int的别名\n        \u002F\u002F DISGUISE 内部对指针做了 ~ 取反操作，“伪装”？\n        disguised_ptr_t disguised_object = DISGUISE(object);\n        \u002F*\n         AssociationsHashMap 继承自 unordered_map，存储 key-value 的组合\n         iterator find ( const key_type& key )，如果 key 存在，则返回key对象的迭代器，\n         如果key不存在，则find返回 unordered_map::end；因此可以通过 `map.find(key) == map.end()`\n         判断 key 是否存在于当前 map 中。\n         *\u002F\n        AssociationsHashMap::iterator i = associations.find(disguised_object);\n        if (i != associations.end()) {\n            \u002F*\n                unordered_map 的键值分别是迭代器的first和second属性。\n                所以说上面先通过 object 对象(实例对象or类对象) 找到其所有关联对象\n                i->second 取到又是一个 ObjectAssociationMap\n                此刻再通过我们自己设定的 key 来查找对应的关联属性值，不过使用\n                `ObjcAssociation` 封装的\n             *\u002F\n            ObjectAssociationMap *refs = i->second;\n            ObjectAssociationMap::iterator j = refs->find(key);\n            if (j != refs->end()) {\n                ObjcAssociation &entry = j->second;\n                value = entry.value();\n                policy = entry.policy();\n                \u002F\u002F 如果策略是 getter retain ，注意这里留个坑\n                \u002F\u002F 平常 OBJC_ASSOCIATION_RETAIN = 01401\n                \u002F\u002F OBJC_ASSOCIATION_GETTER_RETAIN = (1 \u003C\u003C 8)\n                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {\n                    \u002F\u002F TODO: 有学问\n                    objc_retain(value);\n                }\n            }\n        }\n    }\n    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {\n        objc_autorelease(value);\n    }\n    return value;\n}\n```\n\n对应的set操作实现同样简单，耐心看下源码注释，即使不懂 c++ 都没问题：\n\n```c++\nvoid _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {\n    \u002F\u002F retain the new value (if any) outside the lock.\n    ObjcAssociation old_association(0, nil);\n    \u002F\u002F 如果value对象存在，则进行retain or copy 操作\n    id new_value = value ? acquireValue(value, policy) : nil;\n    {\n        AssociationsManager manager;\n        \u002F\u002F manager.associations() 返回的是一个 `AssociationsHashMap` 对象(*_map)\n        \u002F\u002F 所以这里 `&associations` 中用了 `&`\n        AssociationsHashMap &associations(manager.associations());\n        \u002F\u002F intptr_t 是为了兼容平台，在64位的机器上，intptr_t和uintptr_t分别是long int、unsigned long int的别名；在32位的机器上，intptr_t和uintptr_t分别是int、unsigned int的别名\n        \u002F\u002F DISGUISE 内部对指针做了 ~ 取反操作，“伪装”\n        disguised_ptr_t disguised_object = DISGUISE(object);\n        if (new_value) {\n            \u002F\u002F break any existing association.\n            \u002F*\n             AssociationsHashMap 继承自 unordered_map，存储 key-value 的组合\n             iterator find ( const key_type& key )，如果 key 存在，则返回key对象的迭代器，\n             如果key不存在，则find返回 unordered_map::end；因此可以通过 `map.find(key) == map.end()`\n             判断 key 是否存在于当前 map 中。\n             *\u002F\n            AssociationsHashMap::iterator i = associations.find(disguised_object);\n            \u002F\u002F 这里和get操作不同，set操作时如果查询到对象没有关联对象，那么这一次设值是第一次，\n            \u002F\u002F 所以会创建一个新的 ObjectAssociationMap 用来存储实例对象的所有关联属性\n            if (i != associations.end()) {\n                \u002F\u002F secondary table exists\n                \u002F*\n                    unordered_map 的键值分别是迭代器的first和second属性。\n                    所以说上面先通过 object 对象(实例对象or类对象) 找到其所有关联对象\n                    i->second 取到又是一个 ObjectAssociationMap\n                    此刻再通过我们自己设定的 key 来查找对应的关联属性值，不过使用\n                    `ObjcAssociation` 封装的\n                 *\u002F\n                ObjectAssociationMap *refs = i->second;\n                ObjectAssociationMap::iterator j = refs->find(key);\n                \u002F\u002F 关联属性用 ObjcAssociation 结构体封装\n                if (j != refs->end()) {\n                    old_association = j->second;\n                    j->second = ObjcAssociation(policy, new_value);\n                } else {\n                    (*refs)[key] = ObjcAssociation(policy, new_value);\n                }\n            } else {\n                \u002F\u002F create the new association (first time).\n                ObjectAssociationMap *refs = new ObjectAssociationMap;\n                associations[disguised_object] = refs;\n                (*refs)[key] = ObjcAssociation(policy, new_value);\n                \u002F\u002F 知识点是：newisa.has_assoc = true;\n                object->setHasAssociatedObjects();\n            }\n        } else {\n            \u002F\u002F setting the association to nil breaks the association.\n            AssociationsHashMap::iterator i = associations.find(disguised_object);\n            if (i !=  associations.end()) {\n                ObjectAssociationMap *refs = i->second;\n                ObjectAssociationMap::iterator j = refs->find(key);\n                if (j != refs->end()) {\n                    old_association = j->second;\n                    refs->erase(j);\n                }\n            }\n        }\n    }\n    \u002F\u002F release the old value (outside of the lock).\n    if (old_association.hasValue()) ReleaseValue()(old_association);\n}\n```\n\n### 3. 关联对象的如何进行内存管理的？关联对象如何实现weak属性\n\n使用了 `policy` 设置内存管理策略，具体见上。凡是加一层呗。。。搞一个类，然后内部封装一个 `weak` 变量持有；或者不用 weak，但是还是封装一层，但是在 dealloc 中进行置为 nil操作\n\n### 4. `Autoreleasepool`的原理？所使用的的数据结构是什么\n\n基于栈为节点（node）的双向链表，使用 `@autoreleasePool` 包裹的作用域中，所有调用 autorelease 都会将对象push到自动释放池，作用域结束就会drain一次，这里涉及到了 哨兵对象，也就是 插入一个nil标识。\n\n### 5. `ARC`的实现原理？`ARC`下对`retain & release`做了哪些优化\n\nARC 是 LLVM 和 Runtime 协作的结果，ARC 中禁止调用 retain\u002Frelease\u002FretainCount\u002Fdealloc方法，新增weak strong。MRC 是手动管理内存。\n\n简单地说，就是代码中自动加入了retain\u002Frelease，原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。ARC并不是GC，它只是一种代码静态分析（Static Analyzer）工具.比如如果不是 alloc\u002Fnew\u002Fcopy\u002FmutableCopy 开头的函数，编译器会将生成的对象自动放入 autoreleasePool 中。如果是 __strong 修饰的变量，编译器会自动给其加上所有权。等等，详细，我们根据不同的关键字来看看编译器为我们具体做了什么。并从中总结出 ARC 的使用规则。\n\n### 6. `ARC`下哪些情况会造成内存泄漏\n\n* 循环引用;\n* **CF类型内存**\u002FC语言malloc出来的对象；\n* **单例也会造成内存泄漏**\n\n## 其他\n\n1. `Method Swizzle`注意事项\n\n2. 属性修饰符`atomic`的内部实现是怎么样的?能保证线程安全吗\n\n   自旋锁，但实际上是unfair 锁，不能完全保证，因为对于容器变量的修改不能保证线程安全，比如 NSMutableArray。\n\n3. iOS 中内省的几个方法有哪些？内部实现原理是什么\n\n4. `class、objc_getClass、object_getclass` 方法有什么区别?\n\n   ```objective-c\n   \u002F 返回的 isa 指针指向的对象，如果 obj 是实例，返回的是类对象，如果 obj 是类对象，返回的是元类对象\n   \u002F\u002F 如果是元类对象，返回的是 NSObject 元类对象（所有元类对象的isa指针都指向了 NSObject 元类对象）\n   Class object_getClass(id obj)\n   {\n       if (obj) return obj->getIsa();\n       else return Nil;\n   }\n   \n   \u002F\u002F 通过传入类名称 返回类对象\n   Class objc_getClass(const char *aClassName)\n   {\n       if (!aClassName) return Nil;\n   \n       \u002F\u002F NO unconnected, YES class handler\n       return look_up_class(aClassName, NO, YES);\n   }\n   ```\n\n   \n\n# 二、NSNotification相关\n\n认真研读、你可以在这里找到答案[轻松过面：一文全解iOS通知机制(经典收藏)](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5e5fc16df265da575155723b)\n\n1. 实现原理（结构设计、通知如何存储的、`name&observer&SEL`之间的关系等）\n2. 通知的发送时同步的，还是异步的\n3. `NSNotificationCenter`接受消息和发送消息是在一个线程里吗？如何异步发送消息\n4. `NSNotificationQueue`是异步还是同步发送？在哪个线程响应\n5. `NSNotificationQueue`和`runloop`的关系\n6. 如何保证通知接收的线程在主线程\n7. 页面销毁时不移除通知会崩溃吗\n8. 多次添加同一个通知会是什么结果？多次移除通知呢\n9. 下面的方式能接收到通知吗？为什么\n\n```\n\u002F\u002F 发送通知\n[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@\"TestNotification\" object:@1];\n\u002F\u002F 接收通知\n[NSNotificationCenter.defaultCenter postNotificationName:@\"TestNotification\" object:nil];\n复制代码\n```\n\n# 三、Runloop & KVO\n\n## runloop\n\n`runloop`对于一个标准的iOS开发来说都不陌生，应该说熟悉`runloop`是标配，下面就随便列几个典型问题吧\n\n### app如何接收到触摸事件的\n\n> [iOS Touch Event from the inside out](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F70ba981317b6)\n\n#### 1 Touch Event 的生命周期\n\n##### 1.1 物理层面事件的生成\n\niPhone 采用电容触摸传感器，利用人体的电流感应工作，由一块四层复合玻璃屏的内表面和夹层各涂有一层导电层，最外层是一层矽土玻璃保护层。当我们手指触摸感应屏的时候，人体的电场让手指和触摸屏之间形成一个耦合电容，对高频电流来说电容是直接导体。于是手指从接触点吸走一个很小的电流，这个电流分从触摸屏的四脚上的电极流出，并且流经这四个电极的电流和手指到四个电极的距离成正比。控制器通过对这四个电流的比例做精确的计算，得出触摸点的距离。\n\n> 更多文献：\n> * [OLED发光原理、面板结构及OLED关键技术深度解析](https:\u002F\u002Fwww.hangjianet.com\u002Fv5\u002FtopicDetail?id=15422809007930000)\n> * [光学触摸屏原理](http:\u002F\u002Fwww.51touch.com\u002Ftechnology\u002Fprinciple\u002F201308\u002F29-24678.html)\n> * [电容触摸屏原理](http:\u002F\u002Fwww.51touch.com\u002Ftechnology\u002Fprinciple\u002F201308\u002F14-24323.html)\n> * [电阻式触摸屏原理(FLASH演示版)](http:\u002F\u002Fwww.51touch.com\u002Ftechnology\u002Fprinciple\u002F201309\u002F04-24800.html)\n> * [iPhone这十年在传感器上的演进](https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F22677100)\n\n##### 1.2 iOS 操作系统下封装和分发事件\n\niOS 操作系统看做是一个处理复杂逻辑的程序，不同进程之间彼此通信采用消息发送方式，即 IPC (Inter-Process Communication)。现在继续说上面电容触摸传感器产生的 Touch Event，它将交由 IOKit.framework 处理封装成 IOHIDEvent 对象；下一步很自然想到通过消息发送方式将事件传递出去，至于发送给谁，何时发送等一系列的判断逻辑又该交由谁处理呢？\n\n答案是 **SpringBoard.app**，它接收到封装好的 **IOHIDEvent** 对象，经过逻辑判断后做进一步的调度分发。例如，它会判断前台是否运行有应用程序，有则将封装好的事件采用 mach port 机制传递给该应用的主线程。\n\nPort 机制在 IPC 中的应用是 Mach 与其他传统内核的区别之一，在 Mach 中，用户进程调用内核交由 IPC 系统。与直接系统调用不同，用户进程首先向内核申请一个 port 的访问许可；然后利用 IPC 机制向这个 port 发送消息，本质还是系统调用，而处理是交由其他进程完成的。\n\n##### 1.3 IOHIDEvent -> UIEvent\n\n应用程序主线程的 runloop 申请了一个 mach port 用于监听 `IOHIDEvent` 的 `Source1` 事件，回调方法是 `__IOHIDEventSystemClientQueueCallback()`，内部又进一步分发 `Source0` 事件，而 `Source0`事件都是自定义的，非基于端口 port，包括触摸，滚动，selector选择器事件，它的回调方法是 `__UIApplicationHandleEventQueue()`，将接收到的 `IOHIDEvent` 事件对象封装成我们熟悉的 `UIEvent` 事件；然后调用 `UIApplication` 实例对象的 `sendEvent:` 方法，将 `UIEvent` 传递给 `UIWindow` 做一些逻辑判断工作：比如触摸事件产生于哪些视图上，有可能有多个，那又要确定哪个是最佳选项呢？ 等等一系列操作。这里先按下不表。\n\n##### 1.4 Hit-Testing 寻找最佳响应者\n\n`Source0` 回调中将封装好的触摸事件 UIEvent（里面有多个UITouch 即手势点击对象），传递给视图 `UIWindow`，其目的在于找到最佳响应者，这个过程称之为 `Hit-Testing`，字面上理解：hit 即触碰了屏幕某块区域，这个区域可能有多个视图叠加而成，那么这个触摸讲道理响应者有多个喽，那么“最佳”又该如何评判？这里要牢记几个规则：\n\n1. 事件是自下而上传递，即 `UIApplication -> UIWindow -> 子视图 -> ...->子视图中的子视图`;\n2. 后加的视图响应程度更高，即更靠近我们的视图;\n3. 如果某个视图不想响应，则传递给比它响应程度稍低一级的视图，若能响应，你还得继续往下传递，若某个视图能响应了，但是没有子视图 它就是最佳响应者。\n4. 寻找最佳响应者的过程中， UIEvent 中的 UITouch 会不断打上标签：比如 `HitTest View` 是哪个，`superview` 是哪个？关联了什么 `Gesture Recognizer`?\n\n那么如何判定视图为响应者？由于 OC 中的类都继承自 `NSObject` ，因此默认判断逻辑已经在`hitTest:withEvent`方法中实现，它有两个作用： 1.询问当前视图是否能够响应事件 2.事件传递的桥梁。若当前视图无法响应事件，返回 nil 。代码如下：\n\n```swift\n- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ \n  \u002F\u002F 1. 前置条件要满足       \n  if (self.userInteractionEnabled == NO || \n  self.hidden == YES ||  \n  self.alpha \u003C= 0.01) return nil;\n  \n  \u002F\u002F 2. 判断点是否在视图内部 这是最起码的 note point 是在当前视图坐标系的点位置\n    if ([self pointInside:point withEvent:event] == NO) return nil;\n\n  \u002F\u002F 3. 现在起码能确定当前视图能够是响应者 接下去询问子视图\n    int count = (int)self.subviews.count;\n    for (int i = count - 1; i >= 0; i--)\n    {\n      \u002F\u002F 子视图\n        UIView *childView = self.subviews[i];\n    \n    \u002F\u002F 点需要先转换坐标系        \n        CGPoint childP = [self convertPoint:point toView:childView];  \n        \u002F\u002F 子视图开始询问\n        UIView *fitView = [childView hitTest:childP withEvent:event]; \n        if (fitView)\n        {\n      return fitView;\n    }\n    }\n                         \n    return self;\n}\n```\n\n1. 首先满足几个前置条件，可交互`userInteractionEnabled=YES`；没有隐藏`self.hidden == NO`；非透明 `self.alpha \u003C= 0.01` ———— 注意一旦不满足上述三个条件，当前视图及其子视图都不能作为响应者，Hit-Testing 判定也止步于此\n2. 接着判断触摸点是否在视图内部 ———— 这个是最基本，无可厚非的判定规则\n3. 此时已经能够说当前视图为响应者，但是不是**最佳**还不能下定论，因此需要进一步传递给子视图判定；注意 `pointInside` 也是默认实现的。\n\n##### 1.5 UIResponder Chain 响应链\n\n`Hit-Testing` 过程中我们无法确定当前视图是否为“最佳”响应者，此时自然还不能处理事件。因此处理机制应该是找到所有响应者以及最佳响应者(**自下而上**)，由它们构成了一条响应链；接着将事件沿着响应链**自上而下**传递下去 ——最顶端自然是最佳响应者，事件除了被响应者消耗，还能被手势识别器或是 `target-action` 模式捕获并消耗。有时候，最佳响应者可能对处理 `Event` “毫无兴趣”，它们不会重写 `touchBegan` `touchesMove`..等四个方法；也不会添加任何手势；但如果是 `control(控件)` 比如 UIButton ，那么事件还是会被消耗掉的。\n\n##### 1.6 UITouch 、 UIEvent 、UIResponder\n\nIOHIDEvent 前面说到是在 IOKit.framwork 中生成的然后经过一系列的分别才到达前台应用，然后应用主线程runloop处理source1回调中又进行source0事件分发，这里有个封装UIEvent的过程，那么 UITouch 呢？ 是不是也是那时候呢？换种思路：一个手指一次触摸屏幕 生成一个 UITouch 对象，内部应该开始进行识别了，因为可能是多个 Touch，并且触摸的先后顺序也不同，这样识别出来的 UIEvent 也不同。所以 UIEvent 对象中包含了触发该事件的触摸对象的集合，通过 allTouches 属性获取。\n\n每个响应者都派生自 UIResponder 类，本身具有相应事件的能力，响应者默认实现 `touchesBegin` `touchesMove` `touchesEnded` `touchesCancelled`四个方法。\n\n事件在未截断的情况下沿着响应链传递给最佳响应者，伪代码如下：\n\n```swift\n0 - [AView touchesBegan:withEvent\n1 - [UIWindow _sendTouchesForEvent]\n2 - [UIWindow sendEvent]           \n3 - [UIApplication sendEvent]      \n4 __dispatchPreprocessEventFromEventQueue\n5 __handleEventQueueInternal\n6 _CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION_\n7 _CFRunLOOPDoSource0\n8 _CFRunLOOPDoSources0\n9 _CFRunLoopRun\n10 _CFRunLoopRunSpecific\n11 GSEventRunModal\n12 UIApplication\n13 main\n14 start\n\n\u002F\u002F UIApplication.m\n- (void)sendEvent {\n  [window sendEvent];\n}\n\n\u002F\u002F UIWindow.m\n- (void)sendEvent{\n  [self _sendTouchesForEvent];\n}\n\n- (void)_sendTouchesForEvent{\n  \u002F\u002Ffind AView Because we know hitTest View\n  [AView touchesBegan:withEvent];\n}\n```\n\n### 为什么只有主线程的`runloop`是开启的\n\n> 这个问题很奇葩，要回答的会就是其他线程并没有调用 `NSRunLoop *runloop = [NSRunLoop currentRunLoop]`，其他还能说啥呢？更多源码分析见网上教程。\n\n```c\nCFRunLoopRef CFRunLoopGetCurrent(void) {\n    CHECK_FOR_FORK();\n    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);\n    if (rl) return rl;\n    return _CFRunLoopGet0(pthread_self());\n}\n```\n\n### 为什么只在主线程刷新UI\n\n> 引用掘金文章[《iOS拾遗——为什么必须在主线程操作UI》](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c406d97e51d4552475fe178)。\n\nUIKit并不是一个 **线程安全** 的类，UI操作涉及到渲染访问各种View对象的属性，如果异步操作下会存在读写问题，而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点`UIApplication`是在主线程进行初始化，所有的用户事件都是在主线程上进行传递（如点击、拖动），所以 view 只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 **同时** 更新，在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。\n\n### `PerformSelector`和`runloop`的关系\n\nperform 有几种方式，如 `[self performSelector:@selector(perform) withObject:nil]` 同步执行的，等同于 objc_msgSend 方法执行调用方法。\n\n而`[self performSelector:@selector(perform) withObject:nil afterDelay:0]` 则是会在当前 runloop 中起一个 timer，如果当前线程没有起runloop(也就是上面说的没有调用 `[NSRunLoop currentRunLoop]` 方法的话)，则不会有输出\n\n```objective-c\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\n        NSLog(@\"[3]\");\n    });\n}\n\n- (void)perform {\n    NSLog(@\"[2] 线程：%@\",[NSThread currentThread]);\n}\n```\n\n> This method sets up a timer to perform the `aSelector` message on the current thread’s run loop. The timer is configured to run in the default mode (`NSDefaultRunLoopMode`). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode. \n>\n> If you want the message to be dequeued when the run loop is in a mode other than the default mode, use the [performSelector:withObject:afterDelay:inModes:](apple-reference-documentation:\u002F\u002FhcVXEPtYGA)method instead. If you are not sure whether the current thread is the main thread, you can use the [performSelectorOnMainThread:withObject:waitUntilDone:](apple-reference-documentation:\u002F\u002FhcChQUeJuZ) or [performSelectorOnMainThread:withObject:waitUntilDone:modes:](apple-reference-documentation:\u002F\u002FhcNxrNoLOP) method to guarantee that your selector executes on the main thread. To cancel a queued message, use the [cancelPreviousPerformRequestsWithTarget:](apple-reference-documentation:\u002F\u002FhcVCAfekYt) or [cancelPreviousPerformRequestsWithTarget:selector:object:](apple-reference-documentation:\u002F\u002Fhc3jvcW4n6) method.\n\n修改代码：\n\n```objective-c\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        NSRunLoop *runloop = [NSRunLoop currentRunLoop]; \n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\u002F\u002F 这里打断点，po runloop ，查看执行这条语句后runloop添加了啥\n        [runloop run];\n        NSLog(@\"[3]\");\n    });\n}\n```\n\n输出如下内容，注意 `[3]` 被输出了，所以说 runloop 并没有被起来，至于原因见下。\n\n```objective-c\n2020-04-07 00:16:43.146357+0800 04-06-performSelector-RunLoop[15486:585028] [1] 线程：\u003CNSThread: 0x6000000cdd00>{number = 6, name = (null)}\n2020-04-07 00:16:43.146722+0800 04-06-performSelector-RunLoop[15486:585028] [2] 线程：\u003CNSThread: 0x6000000cdd00>{number = 6, name = (null)}\n2020-04-07 00:16:43.146932+0800 04-06-performSelector-RunLoop[15486:585028] [3]\n```\n\n所以为了保活可以加个定时器，repeat=YES 的那种。(ps: repeat=NO 的那种复习下 timer 的循环引用会自动打破！)\n\n```objective-c\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {\n            NSLog(@\"timer 定时任务\");\n        }];\n        \n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        NSRunLoop *runloop = [NSRunLoop currentRunLoop];\n        [runloop addTimer:timer forMode:NSDefaultRunLoopMode];\n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\n        [runloop run];\n        NSLog(@\"[3]\");\n    });\n}\n```\n\nRunLoop 添加观察者代码：\n\n```objective-c\n\u002F*\n kCFRunLoopEntry = (1UL \u003C\u003C 0),1\n kCFRunLoopBeforeTimers = (1UL \u003C\u003C 1),2\n kCFRunLoopBeforeSources = (1UL \u003C\u003C 2), 4\n kCFRunLoopBeforeWaiting = (1UL \u003C\u003C 5), 32\n kCFRunLoopAfterWaiting = (1UL \u003C\u003C 6), 64\n kCFRunLoopExit = (1UL \u003C\u003C 7),128\n kCFRunLoopAllActivities = 0x0FFFFFFFU\n *\u002F\nstatic void addRunLoopObserver() {\n    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {\n        switch (activity) {\n            case kCFRunLoopEntry: {\n                NSLog(@\"即将进入 runloop\");\n            }\n                break;\n            case kCFRunLoopBeforeTimers: {\n                NSLog(@\"定时器 timers 之前\");\n            }\n                break;\n            case kCFRunLoopBeforeSources: {\n                NSLog(@\"Sources 事件前\");\n            }\n                break;\n            case kCFRunLoopBeforeWaiting: {\n                NSLog(@\"RunLoop 即将进入休眠\");\n            }\n                break;\n            case kCFRunLoopAfterWaiting: {\n                NSLog(@\"RunLoop 唤醒后\");\n            }\n                break;\n            case kCFRunLoopExit: {\n                NSLog(@\"退出\");\n            }\n                break;\n            default:\n                break;\n        }\n    });\n  \t\u002F\u002F 这里 CFRunLoopGetCurrent 创建了当前线程的 runloop 对象\n    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);\n    CFRelease(observer);\n}\n\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {\n            NSLog(@\"timer 定时任务\");\n        }];\n        addRunLoopObserver();\n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        NSRunLoop *runloop = [NSRunLoop currentRunLoop];\n        [runloop addTimer:timer forMode:NSDefaultRunLoopMode];\n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\n        [runloop run];\n        NSLog(@\"[3]\");\n    });\n}\n```\n\n>  参考文献：\n>\n>  * [Runloop与performSelector](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c70b391e51d451646267db1)\n\n### 如何使线程保活\n\n> 线程保活就是不让线程退出，所以往简单说就是搞个 “while(1)” 自己实现一套处理流程，事件派发就可以了；但 iOS 中有 runloop，所以我们就无须大费周章。 TODO： C 语言嵌入式如何实现线程池和保活。\n\nrunloop 线程保活前提就是有事情要处理，这里指 timer，source0，source1 事件。\n\n所以有如下几种方式，方式一：\n\n```objective-c\nNSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {\n   NSLog(@\"timer 定时任务\");\n}];\nNSRunLoop *runloop = [NSRunLoop currentRunLoop];\n[runloop addTimer:timer forMode:NSDefaultRunLoopMode];\n[runloop run];\n```\n\n方式二：\n\n```objective-c\nNSRunLoop *runLoop = [NSRunLoop currentRunLoop];\n[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];\n[runLoop run];\n```\n\n方式三：\n\n```objectivec\n- (IBAction)testRunLoopKeepAlive:(id)sender {\n    self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(start) object:nil];\n    [self.myThread start];\n}\n\n- (void)start {\n    self.finished = NO;\n    do {\n      \t\u002F\u002F Runs the loop until the specified date, during which time it processes data from all attached input sources.\n        [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];\n    } while (!self.finished);\n}\n- (IBAction)closeRunloop:(id)sender {\n    self.finished = YES;\n}\n\n- (IBAction)executeTask:(id)sender {\n    [self performSelector:@selector(doTask) onThread:self.myThread withObject:nil waitUntilDone:NO];\n}\n\n- (void)doTask {\n    NSLog(@\"执行任务在线程：%@\",[NSThread currentThread]);\n}\n```\n\n> If no input sources or timers are attached to the run loop, this method exits immediately and returns `NO`; otherwise, it returns after either the first input source is processed or `limitDate` is reached. Manually removing all known input sources and timers from the run loop does not guarantee that the run loop will exit immediately. macOS may install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.\n\n所以上面的方式并非完美，只要没有源，runloop 直接就被退出了，但是因为包了一个while (!self.finished)，所以相当于退出->起->退出-> 起。\n\n> Note:  如果runloop中没有处理事件，这里一直会退出然后起runloop，就算设置了 `[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];` 也没用，但是执行过一次 `[self performSelector:@selector(doTask) onThread:self.myThread withObject:nil waitUntilDone:NO]`，那么程序就卡在 `[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]` 这一行了。\n\n## KVO\n\n同`runloop`一样，这也是标配的知识点了，同样列出几个典型问题\n\n### 1. 实现原理\n\nKVO 会为需要observed的对象动态创建一个子类，以`NSKVONotifying_` 最为前缀，然后将对象的 isa 指针指向新的子类，同时重写 class 方法，返回原先类对象，这样外部就无感知了；其次重写所有要观察属性的setter方法，统一会走一个方法，然后内部是会调用 `willChangeValueForKey` 和 `didChangevlueForKey` 方法，在一个被观察属性发生改变之前， `willChangeValueForKey:`一定会被调用，这就 会记录旧的值。而当改变发生后，`didChangeValueForKey:`会被调用，继而 `observeValueForKey:ofObject:change:context:` 也会被调用。\n\n![图片出处https:\u002F\u002Fjuejin.im\u002Fpost\u002F5adab70cf265da0b736d37a8](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_17f21aeddcec.png)\n\n那么如何验证上面的说法呢？很简单，借助runtime 即可，测试代码请点击[这里](https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record\u002Ftree\u002Fmaster\u002Fsamples\u002F02-25-KVO):\n\n```objective-c\n- (void)viewDidLoad {\n    [super viewDidLoad];\n    self.person = [[Person alloc] initWithName:@\"pmst\" age:18];\n    self.teacher = [[Teacher alloc] initWithName:@\"ppp\" age:28];\n    self.teacher.work = @\"数学\";\n    self.teacher.numberOfStudent = 10;\n    \n    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;\n    \n    RuntimeUtil *utils = [RuntimeUtil new];\n    [utils logClassInfo:self.person.class];\n    [self.person addObserver:self forKeyPath:@\"age\" options:options context:nil];\n    [utils logClassInfo:object_getClass(self.person)];\n    \n    \n    [utils logClassInfo:self.teacher.class];\n    [self.teacher addObserver:self forKeyPath:@\"age\" options:options context:nil];\n    [self.teacher addObserver:self forKeyPath:@\"name\" options:options context:nil];\n    [self.teacher addObserver:self forKeyPath:@\"work\" options:options context:nil];\n    [utils logClassInfo:object_getClass(self.teacher)];\n}\n```\n\n这里 `object_getClass()` 方法实现也贴一下，如果直接使用 `.class` 那么因为被重写过，返回的还是原先对象的类对象，而直接用 runtime 方法的直接返回了 `isa` 指针。\n\n```objective-c\nClass object_getClass(id obj)\n{\n    if (obj) return obj->getIsa();\n    else return Nil;\n}\n```\n\n通过日志确实可以看到子类重写了对应属性的setter方法：\n\n```shell\n2020-03-25 23:11:00.607820+0800 02-25-KVO[28370:1005147] LOG:(NSKVONotifying_Teacher) INFO\n2020-03-25 23:11:00.608190+0800 02-25-KVO[28370:1005147] ==== OUTPUT:NSKVONotifying_Teacher properties ====\n2020-03-25 23:11:00.608529+0800 02-25-KVO[28370:1005147] ==== OUTPUT:NSKVONotifying_Teacher Method ====\n2020-03-25 23:11:00.608876+0800 02-25-KVO[28370:1005147] method name:setWork:\n2020-03-25 23:11:00.609219+0800 02-25-KVO[28370:1005147] method name:setName:\n2020-03-25 23:11:00.646713+0800 02-25-KVO[28370:1005147] method name:setAge:\n2020-03-25 23:11:00.646858+0800 02-25-KVO[28370:1005147] method name:class\n2020-03-25 23:11:00.646971+0800 02-25-KVO[28370:1005147] method name:dealloc\n2020-03-25 23:11:00.647088+0800 02-25-KVO[28370:1005147] method name:_isKVOA\n2020-03-25 23:11:00.647207+0800 02-25-KVO[28370:1005147] =========================\n```\n\n>  疑惑点：看到有文章提出 KVO 之后，setXXX 方法转而调用 `_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify` 等方法，但是通过 runtime 打印 method 是存在的，猜测 SEL 是一样的，但是 IMP 被换掉了，关于源码的实现还未找到。TODO下。\n\n### 2. 如何手动关闭kvo\n\n> KVO 和 KVC 相关接口太多，实际开发中直接查看接口文档即可。\n\n```objective-c\n+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{\n    if ([key isEqualToString:@\"name\"]) {\n        return NO;\n    }else{\n        return [super automaticallyNotifiesObserversForKey:key];\n    }\n}\n\n-(void)setName:(NSString *)name{\n    \n    if (_name!=name) {\n        \n        [self willChangeValueForKey:@\"name\"];\n        _name=name;\n        [self didChangeValueForKey:@\"name\"];\n    }\n      \n}\n```\n\n### 3. 通过KVC修改属性会触发KVO么\n\n会触发 KVO 操作，KVC 时候会先查询对应的 getter 和 setter 方法，如果都没找到，调用 \n\n```objective-c\n+ (BOOL)accessInstanceVariablesDirectly {\n    return NO;\n}\n```\n\n如果返回 YES，那么可以直接修改实例变量。\n\n* KVC 调用 getter 流程：`getKEY，KEY，isKEY, _KEY`，接着是实例变量 `_KEY,_isKEY, KEY, isKEY`;\n\n* KVC 调用 setter 流程：`setKEY`和 `_setKEY`，实例变量顺序 `_KEY,_isKEY, KEY, isKEY`，没找到就调用 `setValue: forUndefinedKey:`\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_c19cd4346db4.jpg)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_94e533300d59.jpg)\n\n### 4. 哪些情况下使用kvo会崩溃，怎么防护崩溃\n\n1. dealloc 没有移除 kvo 观察者，解决方案：创建一个中间对象，将其作为某个属性的观察者，然后dealloc的时候去做移除观察者，而调用者是持有中间对象的，调用者释放了，中间对象也释放了，dealloc 也就移除观察者了；\n2. 多次重复移除同一个属性，移除了未注册的观察者\n3. 被观察者提前被释放，被观察者在 dealloc 时仍然注册着 KVO，导致崩溃。 例如：被观察者是局部变量的情况（iOS 10 及之前会崩溃） 比如 weak ；\n4. 添加了观察者，但未实现 `observeValueForKeyPath:ofObject:change:context:`方法，导致崩溃；\n5. 添加或者移除时 `keypath == nil`，导致崩溃；\n\n> 以下解决方案出自 [iOS 开发：『Crash 防护系统』（二）KVO 防护](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5d67b720f265da039a289bb4) 一文。\n\n**解决方案一：**\n\nFBKVOController 对 KVO 机制进行了额外的一层封装，框架不但可以自动帮我们移除观察者，还提供了 block 或者 selector 的方式供我们进行观察处理。不可否认的是，FBKVOController 为我们的开发提供了很大的便利性。但是相对而言，这种方式对项目代码的侵入性比较大，必须依靠编码规范来强制约束团队人员使用这种方式。\n\n**解决方案二：**\n\n1. 首先为 NSObject 建立一个分类，利用 Method Swizzling，实现自定义的 `BMP_addObserver:forKeyPath:options:context:`、`BMP_removeObserver:forKeyPath:`、`BMP_removeObserver:forKeyPath:context:`、`BMPKVO_dealloc`方法，用来替换系统原生的添加移除观察者方法的实现。\n\n2. 然后在观察者和被观察者之间建立一个 `KVODelegate 对象`，两者之间通过 `KVODelegate 对象` 建立联系。然后在添加和移除操作时，将 KVO 的相关信息例如 `observer`、`keyPath`、`options`、`context` 保存为 `KVOInfo 对象`，并添加到 `KVODelegate 对象` 中对应 的 `关系哈希表` 中，对应原有的添加观察者。 关系哈希表的数据结构：`{keypath : [KVOInfo 对象1, KVOInfo 对象2, ... ]}`\n\n3. 在添加和移除操作的时候，利用 `KVODelegate 对象` 做转发，把真正的观察者变为 `KVODelegate 对象`，而当被观察者的特定属性发生了改变，再由 `KVODelegate 对象` 分发到原有的观察者上。\n\n1. **添加观察者时**：通过关系哈希表判断是否重复添加，只添加一次。\n2. **移除观察者时**：通过关系哈希表是否已经进行过移除操作，避免多次移除。\n3. **观察键值改变时**：同样通过关系哈希表判断，将改变操作分发到原有的观察者上。\n\n**解决方案三：**\n\n**XXShield** 实现方案和 BayMax 系统类似。也是利用一个 Proxy 对象用来做转发， 真正的观察者是 Proxy，被观察者出现了通知信息，由 Proxy 做分发。不过不同点是 Proxy 里面保存的内容没有前者多。只保存了 `_observed（被观察者）` 和关系哈希表，这个关系哈希表中只维护了 `keyPath` 和 `observer` 的关系。\n\n关系哈希表的数据结构：`{keypath : [observer1, observer2 , ...](NSHashTable)}` 。\n\nXXShield 在 dealloc 中也做了类似将多余观察者移除掉的操作，是通过关系数据结构和 `_observed` ，然后调用原生移除观察者操作实现的。\n\n### 5. kvo的优缺点\n\n优点：\n\n1. 运用了设计模式：**观察者模式**\n2. 支持**多个观察者观察同一属性**，或者**一个观察者监听不同属性**。\n3. 开发人员不需要实现属性值变化了发送通知的方案，系统已经封装好了，大大减少开发工作量；\n4. 能够对非我们创建的对象，即内部对象的状态改变作出响应，而且不需要改变内部对象（SDK对象）的实现；\n5. 能够提供观察的属性的最新值以及先前值；\n6. 用key paths来观察属性，因此也可以观察嵌套对象；\n7. 完成了对观察对象的抽象，因为不需要额外的代码来允许观察值能够被观察\n\n缺点：\n\n1. 观察的属性键值硬编码（字符串），编译器不会出现警告以及检查；\n2. 由于允许对一个对象进行不同属性观察，所以在唯一回调方法中，会出现地狱式 `if-else if - else` 分支处理情况；\n\n> References：\n>\n> * [iOS底层原理总结篇-- 深入理解 KVC\\KVO 实现机制](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c2189dee51d454517589c8b)\n> * [iOS 开发：『Crash 防护系统』（二）KVO 防护](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5d67b720f265da039a289bb4)\n> *  [ValiantCat](https:\u002F\u002Fgithub.com\u002FValiantCat) \u002F **[XXShield](https:\u002F\u002Fgithub.com\u002FValiantCat\u002FXXShield)**（第三方框架）\n> *  [JackLee18](https:\u002F\u002Fgithub.com\u002FJackLee18) \u002F **[JKCrashProtect](https:\u002F\u002Fgithub.com\u002FJackLee18\u002FJKCrashProtect)**（第三方框架）\n> * [大白健康系统 -- iOS APP运行时 Crash 自动修复系统](https:\u002F\u002Fneyoufan.github.io\u002F2017\u002F01\u002F13\u002Fios\u002FBayMax_HTSafetyGuard)\n\n# 四、Block\n\n1. `block`的内部实现，结构体是什么样的\n\n   block 也是一个对象，主要分为 Imp 结构体 和 Desc 结构体，用 `clang -rewrite-objc` 命令将 oc 代码重写成 c++:\n\n   ```c++\n   struct __block_impl {\n     void *isa;\n     int Flags;\n     int Reserved;\n     void *FuncPtr;\n   };\n   \n   struct __main_block_impl_0 {\n     struct __block_impl impl;\n     struct __main_block_desc_0* Desc;\n     __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {\n       impl.isa = &_NSConcreteStackBlock;\n       impl.Flags = flags;\n       impl.FuncPtr = fp;\n       Desc = desc;\n     }\n   };\n   ```\n\n   > 重写c++只是帮助我们理解，实际实现还是有偏差的，转成 IR 是不是更好呢？\n\n2. block是类吗，有哪些类型\n\n   一般，block有三种：_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock，根据Block对象创建时所处数据区不同而进行区别。\n\n   1. 栈上 Block，引用了栈上变量，生命周期由系统控制的，一旦所属作用域结束，就被系统销毁了。\n   2. 堆上 Block，使用 copy 或者 strong（ARC）下就从栈Block 拷贝到堆上。\n   3. 全局 Block，未引用任何栈上变量时就是全局Block;\n\n3. 一个`int`变量被 `__block` 修饰与否的区别？block的变量截获\n\n   值 copy 和指针 copy，`__block` 修饰的话允许在 block 内部修改变量，因为传入的是 int变量的指针。\n\n   外部变量有四种类型：自动变量、静态变量、静态全局变量、全局变量。\n\n   全局变量和静态全局变量在 block 中是直接引用的，不需要通过结构去传入指针；\n\n   函数\u002F方法中的 static 静态变量是直接在block中保存了指针，如下测试代码：\n\n   ```c++\n   int a = 1;\n   static int b = 2;\n   \n   int main(int argc, const char * argv[]) {\n   \n       int c = 3;\n       static int d = 4;\n       NSMutableString *str = [[NSMutableString alloc]initWithString:@\"hello\"];\n       void (^blk)(void) = ^{\n           a++;\n           b++;\n           d++;\n           [str appendString:@\"world\"];\n           NSLog(@\"1----------- a = %d,b = %d,c = %d,d = %d,str = %@\",a,b,c,d,str);\n       };\n       \n       a++;\n       b++;\n       c++;\n       d++;\n   str = [[NSMutableString alloc]initWithString:@\"haha\"];\n       NSLog(@\"2----------- a = %d,b = %d,c = %d,d = %d,str = %@\",a,b,c,d,str);\n       blk();\n       \n       return 0;\n   }\n   ```\n\n   转成  c++ 代码：\n\n   ```objective-c\n   struct __block_impl {\n     void *isa;\n     int Flags;\n     int Reserved;\n     void *FuncPtr;\n   };\n   \n   int a = 1; \u002F\u002F \u003C------------------- NOTE\n   static int b = 2; \u002F\u002F \u003C------------------- NOTE\n   struct __main_block_impl_0 {\n     struct __block_impl impl;\n     struct __main_block_desc_0* Desc;\n     int *d;\t\t\t\t\t\t\u002F\u002F \u003C------------------- NOTE\n     NSMutableString *str;\t\t\t\t\u002F\u002F \u003C------------------- NOTE\n     int c; \u002F\u002F \u003C------------------- NOTE\n     __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {\n       impl.isa = &_NSConcreteStackBlock;\n       impl.Flags = flags;\n       impl.FuncPtr = fp;\n       Desc = desc;\n     }\n   };\n   \n   static void __main_block_func_0(struct __main_block_impl_0 *__cself) {\n     int *d = __cself->d; \u002F\u002F bound by copy\n     NSMutableString *str = __cself->str; \u002F\u002F bound by copy\n     int c = __cself->c; \u002F\u002F bound by copy\n   \n           a++;\n           b++;\n           (*d)++;\n           ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName(\"appendString:\"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_1);\n           NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_2,a,b,c,(*d),str);\n       }\n   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\u002F*BLOCK_FIELD_IS_OBJECT*\u002F);}\n   \n   static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3\u002F*BLOCK_FIELD_IS_OBJECT*\u002F);}\n   \n   static struct __main_block_desc_0 {\n     size_t reserved;\n     size_t Block_size;\n     void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);\n     void (*dispose)(struct __main_block_impl_0*);\n   } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};\n   \n   int main(int argc, const char * argv[]) {\n       int c = 3;\n       static int d = 4;\n       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_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_0);\n       void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));\n   \n       a++;\n       b++;\n       c++;\n       d++;\n       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_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);\n       NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_4,a,b,c,d,str);\n       ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);\n   \n       return 0;\n   }\n   ```\n\n   \n\n4. `block`在修改`NSMutableArray`，需不需要添加`__block`\n\n   不需要，本身 block 内部就捕获了 NSMutableArray 指针，除非你要修改指针指向的对象，而这里明显只是修改内存数据，这个可以类比 NSMutableString。\n\n5. 怎么进行内存管理的\n\n   `static void *_Block_copy_internal(const void *arg, const int flags)` 和 `void _Block_release(void *arg) `\n\n   > 推荐[iOS Block原理探究以及循环引用的问题](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F9ff40ea1cee5) 一文。\n\n6. `block`可以用`strong`修饰吗\n\n   ARC 貌似是可以的， strong 和 copy 的操作都是将栈上block 拷贝到堆上。TODO：确认下。\n\n7. 解决循环引用时为什么要用`__strong、__weak`修饰\n\n   `__weak` 就是为了避免 retainCycle，而block 内部 `__strong` 则是在作用域 retain 持有当前对象做一些操作，结束后会释放掉它。\n\n8. `block`发生`copy`时机\n\n   block 从栈上拷贝到堆上几种情况：\n\n   * 调用Block的copy方法\n\n   * 将Block作为函数返回值时\n\n   * 将Block赋值给__strong修饰的变量或Block类型成员变量时\n\n   * 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时\n\n9. `Block`访问对象类型的`auto变量`时，在`ARC和MRC`下有什么区别\n\n# 四、Block 原理探究代码篇\n\n首先明确 Block 底层数据结构，之后所有的 demos 都基于此来学习知识点：\n\n```c\ntypedef NS_OPTIONS(int,PTBlockFlags) {\n    PTBlockFlagsHasCopyDisposeHelpers = (1 \u003C\u003C 25),\n    PTBlockFlagsHasSignature          = (1 \u003C\u003C 30)\n};\ntypedef struct PTBlock {\n    __unused Class isa;\n    PTBlockFlags flags;\n    __unused int reserved;\n    void (__unused *invoke)(struct PTBlock *block, ...);\n    struct {\n        unsigned long int reserved;\n        unsigned long int size;\n        \u002F\u002F requires PTBlockFlagsHasCopyDisposeHelpers\n        void (*copy)(void *dst, const void *src);\n        void (*dispose)(const void *);\n        \u002F\u002F requires PTBlockFlagsHasSignature\n        const char *signature;\n        const char *layout;\n    } *descriptor;\n    \u002F\u002F imported variables\n  \t\u002F\u002F Block 捕获的实例变量都在次\n} *PTBlockRef;\n\ntypedef struct PTBlock_byref {\n    void *isa;\n    struct PTBlock_byref *forwarding;\n    volatile int flags; \u002F\u002F contains ref count\n    unsigned int size;\n    \u002F\u002F 下面两个函数指针是不定的 要根据flags来\n\u002F\u002F    void (*byref_keep)(struct PTBlock_byref *dst, struct PTBlock_byref *src);\n\u002F\u002F    void (*byref_destroy)(struct PTBlock_byref *);\n    \u002F\u002F long shared[0];\n} *PTBlock_byref_Ref;\n```\n\n## 1. 调用 block\n\n```c\nvoid (^blk)(void) = ^{\n  NSLog(@\"hello world\");\n};\nPTBlockRef block = (__bridge PTBlockRef)blk;\nblock->invoke(block);\n```\n\n## 2. block 函数签名\n\n```c\nvoid (^blk)(int, short, NSString *) = ^(int a, short b, NSString *str){\n  NSLog(@\"a:%d b:%d str:%@\",a,b,str);\n};\nPTBlockRef block = (__bridge PTBlockRef)blk;\nif (block->flags & PTBlockFlagsHasSignature) {\n  void *desc = block->descriptor;\n  desc += 2 * sizeof(unsigned long int);\n  if (block->flags & PTBlockFlagsHasCopyDisposeHelpers) {\n    desc += 2 * sizeof(void *);\n  }\n\n  const char *signature = (*(const char **)desc);\n  NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:signature];\n  NSLog(@\"方法 signature:%s\",signature);\n}\n\n\u002F\u002F 打印内容如下:\n\u002F\u002F v24 @?0 i8 s12 @\"NSString\"16\n\u002F\u002F 其中 ? 是 An unknown type (among other things, this code is used for function pointers)\n```\n\n## 3. block 捕获栈上局部变量\n\n捕获的变量都会按照顺序放置在 `PTBlock` 结构体后面，如此看来就是个变长结构体。\n\n也就是说我们可以通过如下方式知道 block 捕获了哪些外部变量（全局变量除外）。\n\n```c\nint a = 0x11223344;\nint b = 0x55667788;\nNSString *str = @\"pmst\";\nvoid (^blk)(void) = ^{\n  NSLog(@\"a:%d b:%d str:%@\",a,b, str);\n};\nPTBlockRef block = (__bridge PTBlockRef)blk;\nvoid *pt = (void *)block + sizeof(struct PTBlock);\nlong long *ppt = pt;\nNSString *str_ref = (__bridge id)((void *)(*ppt));\nint *a_ref = pt + sizeof(NSString *);\nint *b_ref = pt + sizeof(NSString *) + sizeof(int);\n\nNSLog(@\"a:0x%x b:0x%x str:%@\",*a_ref, *b_ref, str_ref);\n```\n\n> TODO：`NSString` layout 布局为何在第一位？\n\n## 4. `__block` 变量（栈上）\n\n```c\n__block int a = 0x99887766;\n__unsafe_unretained void (^blk)(void) = ^{\n  NSLog(@\"__block a :%d\",a);\n};\nNSLog(@\"Block 类型 %@\",[blk class]);\nPTBlockRef block = (__bridge PTBlockRef)blk;\nvoid *pt = (void *)block + sizeof(struct PTBlock);\nlong long *ppt = pt;\nvoid *ref = (PTBlock_byref_Ref)(*ppt);\nvoid *shared = ref + sizeof(struct PTBlock_byref);\nint *a_ref = (int *)shared;\nNSLog(@\"a 指针：%p block a 指针:%p block a value:0x%x\",&a, a_ref,*a_ref);\nNSLog(@\"PTBlock_byref 指针：%p\",ref);\nNSLog(@\"PTBlock_byref forwarding 指针：%p\",((PTBlock_byref_Ref)ref)->forwarding);\n\u002F*\n输出如下：\nBlock 类型 __NSStackBlock__\na 指针：0x7ffeefbff528 block a 指针:0x7ffeefbff528 block a value:0x99887766\nPTBlock_byref 指针：0x7ffeefbff510\nPTBlock_byref forwarding 指针：0x7ffeefbff510\n*\u002F\n```\n\n可以看到 `__block int a` 已经变成了另外一个数据结构了，打印地址符合预期，此刻 block 以及其他的变量结构体都在栈上。\n\n## 5.  `__block` 变量，[block copy] 后的内存变化\n\n```c\n__block int a = 0x99887766;\n__unsafe_unretained void (^blk)(NSString *) = ^(NSString *flag){\n  NSLog(@\"[%@] 中 a 地址:%p\",flag, &a);\n};\nNSLog(@\"blk 类型 %@\",[blk class]);\nblk(@\"origin block\");\nvoid (^copyblk)(NSString *) = [blk copy];\ncopyblk(@\"copy block\");\nblk(@\"origin block 二次调用\");\n\u002F**\n\t输出如下：\nblk 类型 __NSStackBlock__\n[origin block] 中 a 地址:0x7ffeefbff528\ncopyblk 类型 __NSMallocBlock__\n[copy block] 中 a 地址:0x102212468\n[origin block 二次调用] 中 a 地址:0x102212468\n*\u002F\n```\n\n很明显对 blk 进行 copy 操作后，copyblk 已经“移驾”到堆上，随着拷贝的还有 `__block` 修饰的a变量（`PTBlock_byref_Ref `类型）；\n\n## 6. `__block` 变量中 forwarding 指针\n\n```c\n__block int a = 0x99887766;\n__unsafe_unretained void (^blk)(NSString *,id) = ^(NSString *flag, id bblk){\n  NSLog(@\"[%@] a address:%p\",flag, &a); \u002F\u002F a 取值都是 ->forwarding->a 方式\n  PTBlockRef block = (__bridge PTBlockRef)bblk;\n  void *pt = (void *)block + sizeof(struct PTBlock);\n  long long *ppt = pt;\n  void *ref = (PTBlock_byref_Ref)(*ppt);\n  NSLog(@\"[%@] PTBlock_byref_Ref 指针：%p\",flag,ref);\n  NSLog(@\"[%@] PTBlock_byref_Ref forwarding 指针：%p\",flag,((PTBlock_byref_Ref)ref)->forwarding);\n  void *shared = ref + sizeof(struct PTBlock_byref);\n  int *a_ref = (int *)shared;\n  NSLog(@\"[%@] a value : 0x%x a adress:%p\", flag, *a_ref, a_ref);\n\n};\nNSLog(@\"blk 类型 %@\",[blk class]);\nblk(@\"origin block\", blk);\nvoid (^copyblk)(NSString *,id) = [blk copy];\nNSLog(@\"copyblk 类型 %@\",[copyblk class]);\ncopyblk(@\"copy block\",copyblk);\nblk(@\"origin block after copy\", blk);\n\u002F**\nMRC 模式下输出：\nblk 类型 __NSStackBlock__\n[origin block] a address:0x7ffeefbff528\n[origin block] PTBlock_byref_Ref 指针：0x7ffeefbff510\n[origin block] PTBlock_byref_Ref forwarding 指针：0x7ffeefbff510\n[origin block] a value : 0x99887766 a adress:0x7ffeefbff528\ncopyblk 类型 __NSMallocBlock__\n[copy block] a address:0x1032041d8\n[copy block] PTBlock_byref_Ref 指针：0x1032041c0\n[copy block] PTBlock_byref_Ref forwarding 指针：0x1032041c0\n[copy block] a value : 0x99887766 a adress:0x1032041d8\n[origin block after copy] a address:0x1032041d8\n[origin block after copy] PTBlock_byref_Ref 指针：0x7ffeefbff510\n[origin block after copy] PTBlock_byref_Ref forwarding 指针：0x1032041c0\n[origin block after copy] a value : 0x99887766 a adress:0x7ffeefbff528\n\nARC 模式下输出（这个稍有出路）：\nblk 类型 __NSStackBlock__\n[origin block] a address:0x100604cc8\n[origin block] PTBlock_byref_Ref 指针：0x100604cb0\n[origin block] PTBlock_byref_Ref forwarding 指针：0x100604cb0\n[origin block] a value : 0x99887766 a adress:0x100604cc8\ncopyblk 类型 __NSMallocBlock__\n[copy block] a address:0x100604cc8\n[copy block] PTBlock_byref_Ref 指针：0x100604cb0\n[copy block] PTBlock_byref_Ref forwarding 指针：0x100604cb0\n[copy block] a value : 0x99887766 a adress:0x100604cc8\n*\u002F\n```\n\n这里可以看到 forwarding 指针确实指向了结构体本身，随着 copy 行为确实进行了一次栈->堆的赋值——`block`和 `__block` 变量。\n\n> 建议用 lldb 命令去看内存布局。\n\n## 7. Block Hook\n\nTODO:\n\n\n\n# 五、多线程\n\n# \n\n## `iOS`开发中有多少类型的线程？分别对比\n\n* NSThread，每个 NSThread对象对应一个线程，量级较轻，通常我们会起一个 runloop 保活，然后通过添加自定义source0源或者 perform onThread 来进行调用，优点轻量级，使用简单，缺点：需要自己管理线程的生命周期，保活，另外还会线程同步，加锁、睡眠和唤醒。\n* GCD：Grand Central Dispatch（派发） 是基于C语言的框架，可以充分利用多核，是苹果推荐使用的多线程技术\n  * 优点：GCD更接近底层，而NSOperationQueue则更高级抽象，所以GCD在追求性能的底层操作来说，是速度最快的，有待确认\n  * 缺点：操作之间的事务性，顺序行，依赖关系。GCD需要自己写更多的代码来实现\n* NSOperation\n  * 优点： 使用者的关注点都放在了 operation 上，而不需要线程管理。\n    * 支持在操作对象之间依赖关系，方便控制执行顺序。\n    * 支持可选的完成块，它在操作的主要任务完成后执行。\n    * 支持使用KVO通知监视操作执行状态的变化。\n    * 支持设定操作的优先级，从而影响它们的相对执行顺序。\n    * 支持取消操作，允许您在操作执行时暂停操作。\n  * 缺点：高级抽象，性能方面相较 GCD 来说不足一些;\n\n## `GCD`有哪些队列，默认提供哪些队列\n\n> [Grand Central Dispatch(GCD) 深入浅出](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F8cb4f395d2c4)\n\n| 队列                                               | 队列类型     | 说明                                                         |\n| -------------------------------------------------- | ------------ | ------------------------------------------------------------ |\n| 主队列（main queue）                               | 串行         | 保证所有的任务都在主线程执行，而主线程是唯一用于 UI 更新的线程。此外还用于发送消息给视图或发送通知。 |\n| 四个全局调度队列（high、default、low、background） | 并发         | Apple 的接口也会使用这些队列，所以你添加的任何任务都不会是这些队列中唯一的任务 |\n| 自定义队列                                         | 串行 or 并发 | 1. 多个任务以串行方式执行，但又不想在主线程中；2. 多个任务以并行方式执行，但不希望队列中有其他系统的任务干扰。 |\n\n## `GCD`有哪些方法api\n\n```objective-c\n\u002F\u002F 接口过多，罗列几个关键的\n\u002F\u002F 1. dispatch_sync\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    \u002F\u002F 同步 \n    dispatch_sync(\u003C#dispatch_queue_t  _Nonnull queue#>, \u003C#^(void)block#>)\n    \n    \u002F\u002F 由于是同步，线程A会被阻塞 \n    [self doOtherThing];\n}\n\n\u002F\u002F 2. dispatch_async\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    \u002F\u002F 异步 \n    dispatch_async(\u003C#dispatch_queue_t  _Nonnull queue#>, \u003C#^(void)block#>)\n    \n    \u002F\u002F 由于是异步，线程A不会被阻塞\n    [self doOtherThing];\n}\n\n\u002F\u002F 3. dispatch_after\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(\u003C#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{\n        \u003C#code to be executed after a specified delay#>\n    });\n}\n\n\u002F\u002F 4. dispatch_once\nstatic dispatch_once_t onceToken;\ndispatch_once(&onceToken, ^{\n    \u003C#code to be executed once#>\n});\n\n\u002F\u002F 5. dispatch_barrier_sync 和 dispatch_barrier_async\n\u002F\u002F 这个可以实现读写锁\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    \u002F\u002F 同步 \n    dispatch_barrier_sync(\u003C#dispatch_queue_t  _Nonnull queue#>, \u003C#^(void)block#>)\n    \n    \u002F\u002F 由于是同步，线程A会被阻塞 \n    [self doOtherThing];\n}\n\n\u002F\u002F 6. dispatch_apply\n\u002F\u002F 串行方式做事情\n- (void)serialDoSomething {\n  for(int idx=0; idx \u003C 3; idx++) {\n    \u002F\u002F 这里你可以处理事情 比如下载图片\n    downloadPic(idx);\n  }\n}\n\n\u002F\u002F 并发方式做事情\n- (void)concurrencyDoSomething {\n  dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t idx) {\n        \u002F\u002F 由于下载图片之间没有任何关系，允许并发的去下载\n        downloadPic(idx);\n    })  \n}\n```\n\n7. Dispatch Groups 集合\n\n这里引入“**组(group)**”的概念，与队列不同，任何加入到组中的任务(task)，可以是串行执行或并行执行，可以来自任何其他队列，当组中所有任务完成之时，会通知你这个消息。下面是几个常用接口：\n\n- `dispatch_group_t group_name = dispatch_group_create();` 实例化一个组（就是个信号量）\n- `dispatch_group_enter(\u003C#dispatch_group_t _Nonnull group#>)` 和 `dispatch_group_leave(\u003C#dispatch_group_t _Nonnull group#>)` ，“加入”和“离开”是一对，就好比Objective-C 内存管理一样，谁持有(`retain`)谁释放(`release`)\n- `dispatch_group_wait(\u003C#dispatch_group_t _Nonnull group#>,DISPATCH_TIME_FOREVER)` 阻塞当前线程，等待任务组中的所有任务执行完毕。\n- `dispatch_group_notify(\u003C#dispatch_group_t _Nonnull group#>, \u003C#dispatch_queue_t _Nonnull queue#>, \u003C#^(void)block#>)` 和3不同，当组中的全部执行完毕，将 `block` 任务加入到队列 `queue` 执行。\n\n8. Semaphores 信号量\n\n```objective-c\ndispatch_group_t group = dispatch_group_create();   \u002F\u002F 1\ndispatch_semaphore_t semaphore = dispatch_semaphore_create(10);   \u002F\u002F 2\ndispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); \u002F\u002F 3  \nfor (int i = 0; i \u003C 100; i++)   \n{   \n    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);   \u002F\u002F 4 \n    dispatch_group_async(group, queue, ^{   \n        NSLog(@\"%i\",i);    \u002F\u002F 5\n        sleep(2); \n        dispatch_semaphore_signal(semaphore);   \u002F\u002F 6\n    });   \n}   \ndispatch_group_wait(group, DISPATCH_TIME_FOREVER);\u002F\u002F 7\nNSLog(@\"所有任务完成\");\n```\n\n## `GCD`主线程 & 主队列的关系\n\n队列其实就是一个数据结构体，主队列由于是串行队列，所以入队列中的 task 会逐一派发到主线程中执行；但是其他队列也可能会派发到主线程执行\n\n## 如何实现同步，有多少方式就说多少\n\n1. dispatch_sync\n2. dispatch_group，\n3. dispatch_semaphore\n4. NSLock\n5. pthread_mutex_t 互斥锁、递归锁等\n6.  @synchronized\n\n## `dispatch_once`实现原理\n\n```c\nvoid\ndispatch_once(dispatch_once_t *val, dispatch_block_t block)\n{\n\tdispatch_once_f(val, block, _dispatch_Block_invoke(block));\n}\n\nvoid\ndispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)\n{\n#if !DISPATCH_ONCE_INLINE_FASTPATH\n\tif (likely(os_atomic_load(val, acquire) == DLOCK_ONCE_DONE)) {\n\t\treturn;\n\t}\n#endif \u002F\u002F !DISPATCH_ONCE_INLINE_FASTPATH\n\treturn dispatch_once_f_slow(val, ctxt, func);\n}\n\nstatic void\ndispatch_once_f_slow(dispatch_once_t *val, void *ctxt, dispatch_function_t func)\n{\n\t_dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;\n\tstruct _dispatch_once_waiter_s dow = { };\n\t_dispatch_once_waiter_t tail = &dow, next, tmp;\n\tdispatch_thread_event_t event;\n\n\tif (os_atomic_cmpxchg(vval, NULL, tail, acquire)) {\n\t\tdow.dow_thread = _dispatch_tid_self();\n\t\t_dispatch_client_callout(ctxt, func);\n\n\t\tnext = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);\n\t\twhile (next != tail) {\n\t\t\ttmp = (_dispatch_once_waiter_t)_dispatch_wait_until(next->dow_next);\n\t\t\tevent = &next->dow_event;\n\t\t\tnext = tmp;\n\t\t\t_dispatch_thread_event_signal(event);\n\t\t}\n\t} else {\n\t\t_dispatch_thread_event_init(&dow.dow_event);\n\t\tnext = *vval;\n\t\tfor (;;) {\n\t\t\tif (next == DISPATCH_ONCE_DONE) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (os_atomic_cmpxchgv(vval, next, tail, &next, release)) {\n\t\t\t\tdow.dow_thread = next->dow_thread;\n\t\t\t\tdow.dow_next = next;\n\t\t\t\tif (dow.dow_thread) {\n\t\t\t\t\tpthread_priority_t pp = _dispatch_get_priority();\n\t\t\t\t\t_dispatch_thread_override_start(dow.dow_thread, pp, val);\n\t\t\t\t}\n\t\t\t\t_dispatch_thread_event_wait(&dow.dow_event);\n\t\t\t\tif (dow.dow_thread) {\n\t\t\t\t\t_dispatch_thread_override_end(dow.dow_thread, val);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t_dispatch_thread_event_destroy(&dow.dow_event);\n\t}\n}\n```\n\n上面是 libdispatch-913.60.2 的实现，稍显复杂，所以找了下旧版本的[其他大佬的解答摘抄下](http:\u002F\u002Flingyuncxb.com\u002F2018\u002F02\u002F01\u002FGCD源码分析2%20——%20dispatch-once篇\u002F)：\n\n```c\nvoid dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){\n    \n    volatile long *vval = val;\n    if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {\n        func(ctxt); \u002F\u002F block真正执行\n        dispatch_atomic_barrier();\n        *val = ~0l;\n    } \n    else \n    {\n        do\n        {\n            _dispatch_hardware_pause();\n        } while (*vval != ~0l);\n        dispatch_atomic_barrier();\n    }\n}\n```\n\n- 1、在开篇中已经讲过`dispatch_atomic_cmpxchg`，它是一个宏定义，原型为`__sync_bool_compare_and_swap((p), (o), (n))` ，这是LockFree给予CAS的一种原子操作机制，原理就是 **如果p==o，那么将p设置为n，然后返回true;否则，不做任何处理返回false**( 博主这里小声逼逼：其实就是gcc提供的底层原子操作，CPU提供了在指令执行期间对总线加锁的手段，CPU芯片有一个引脚只要拉低电平就可以组织其他指令进行通过总线访问内存了，只有当前面的值指令执行完毕，把引脚电平复位就又恢复正常，这个就是硬件上的实现。)\n- 2、在多线程环境中，如果某一个线程A首次进入`dispatch_once_f`，*val==0，这个时候直接将其原子操作设为1，然后执行传入`dispatch_once_f`的block，然后调用`dispatch_atomic_barrier`，最后将*val的值修改为~0。\n- 3、`dispatch_atomic_barrier`是一种内存屏障，所谓内存屏障，从处理器角度来说，是用来串行化读写操作的，从软件角度来讲，就是用来解决顺序一致性问题的。编译器不是要打乱代码执行顺序吗，处理器不是要乱序执行吗，你插入一个内存屏障，就相当于告诉编译器，屏障前后的指令顺序不能颠倒，告诉处理器，只有等屏障前的指令执行完了，屏障后的指令才能开始执行。所以这里`dispatch_atomic_barrier`能保证只有在block执行完毕后才能修改*val的值。\n- 4、在首个线程A执行block的过程中，如果其它的线程也进入`dispatch_once_f`，那么这个时候if的原子判断一定是返回false，于是走到了else分支，于是执行了do~while循环，其中调用了`_dispatch_hardware_pause`，这有助于提高性能和节省CPU耗电，pause就像nop，干的事情就是延迟空等的事情。直到首个线程已经将block执行完毕且将*val修改为~0，调用`dispatch_atomic_barrier`后退出。这么看来其它的线程是无法执行block的，这就保证了在`dispatch_once_f`的block的执行的唯一性，生成的单例也是唯一的。\n\ndispatch_once死锁\n\n- 死锁方式1：\n  1、某线程T1()调用单例A，且为应用生命周期内首次调用，需要使用dispatch_once(&token, block())初始化单例。\n  2、上述block()中的某个函数调用了dispatch_sync_safe，同步在T2线程执行代码\n  3、T2线程正在执行的某个函数需要调用到单例A，将会再次调用dispatch_once。\n  4、这样T1线程在等block执行完毕，它在等待T2线程执行完毕，而T2线程在等待T1线程的dispatch_once执行完毕，造成了相互等待，故而死锁\n- 死锁方式2：\n  1、某线程T1()调用单例A，且为应用生命周期内首次调用，需要使用dispatch_once(&token, block())初始化单例；\n  2、block中可能掉用到了B流程，B流程又调用了C流程，C流程可能调用到了单例A，将会再次调用dispatch_once；\n  3、这样又造成了相互等待。\n\n所以在使用写单例时要注意：\n\n- 1、初始化要尽量简单，不要太复杂；\n- 2、尽量能保持自给自足，减少对别的模块或者类的依赖；\n- 3、单例尽量考虑使用场景，不要随意实现单例，否则这些单例一旦初始化就会一直占着资源不能释放，造成大量的资源浪费。\n\n> [**深入浅出 GCD 之 dispatch_once**](https:\u002F\u002Fxiaozhuanlan.com\u002Ftopic\u002F7916538240)\n\n## 什么情况下会死锁\n\n线程死锁是指由于两个或者多个线程互相持有对方所需要的资源，导致这些线程处于等待状态，无法前往执行。当线程互相持有对方所需要的资源时，会互相等待对方释放资源，如果线程都不主动释放所占有的资源，将产生死锁。\n\n\n\n## 有哪些类型的线程锁，分别介绍下作用和使用场景\n\n[[iOS] 谈谈iOS多线程的锁](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5a0a92996fb9a0451f307479) 一文直接吃透所有锁。\n\n## `NSOperationQueue`中的`maxConcurrentOperationCount`默认值\n\n默认值为 -1，默认的最大操作数由NSOperationQueue对象根据当前系统条件动态确定。\n\n## `NSTimer、CADisplayLink、dispatch_source_t` 的优劣\n\n### NSTimer\n\n方式一：\n\n> 接口文档：Creates a timer and schedules it on the current run loop in the default mode.\n\n```objective-c\nself.timer = [NSTimer scheduledTimerWithTimeInterval:2\n              target:self\n              selector:@selector(timerTest)\n              userInfo:nil\n              repeats:YES];\n\u002F\u002F 我们可以通过如下接口添加到commonMode中\n[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];\n```\n\n方式二：\n\n> You must add the new timer to a run loop, using [addTimer:forMode:](apple-reference-documentation:\u002F\u002FhcocJkO-uk). Then, after `ti` seconds have elapsed, the timer fires, sending the message `aSelector` to `target`. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)\n\n```objective-c\nself.timer = [NSTimer timerWithTimeInterval:2\n              target:self\n              selector:@selector(timerTest)\n              userInfo:nil\n              repeats:YES];\n[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];\n```\n\n> 关于 NSTimer 的 retainCycle 问题和解决方案可参考其他文章。解决方案有：\n>\n> 1.  NSTimer 加一个 category，然后以 block 的方式注入定时器触发时的执行任务，Timer 的 target 此处是用 Timer 类对象，而它是常驻内存中的，所以vc->timer->Timer类对象 没有构成环，但是注意闭包中要用 weakSelf；\n> 2.  封装一个中间对象 WeakProxy， 内部使用一个 weak 属性变量持有 self，所以现在持有关系式 vc->timer->weakProxy ---->vc，所以也没有形成 retainCycle。\n>\n> NSTimer repeats 等于 NO 的时候，执行完任务，定时器自动 invalidated 就会释放对 self 的 strong reference ，\n\n销毁的时候使用：\n\n```objective-c\n\u002F\u002F\u002F 文档接口说明：The timer maintains a strong reference to this object until it (the timer) is invalidated.\n[self.timer invalidate];\nself.timer = nil;\n```\n\n**缺点**\n\n**计时不精确**：不管是一次性的还是周期性的timer的实际触发事件的时间，都会与所加入的**RunLoop**和**RunLoop Mode**有关，如果此**RunLoop**正在执行一个连续性的运算，**timer**就会被延时出发。重复性的**timer**遇到这种情况，如果延迟超过了一个周期，则会在延时结束后立刻执行，并按照之前指定的周期继续执行。\n\n### CADisplayLink\n\n> Apple 专门提供的一个类，主要的优势在于他的执行频率是根据设备屏幕的刷新频率来计算的，也即是时间间隔最准确的定时器。用法和 NSTimer 差不多，当然也存在 retainCycle 问题，解决方式同上。\n\n暂停继续：\n\n```objective-c\n- (void)displayLinkStart {\n    self.displayLink.paused = !self.displayLink.paused;\n}\n```\n\n#### 优缺点\n\n- **优点：** 依托于设备屏幕刷新频率触发事件，所以其触发时间上是最准确的。也是最适合做UI不断刷新的事件，过渡相对流畅，无卡顿感。\n- **缺点：**\n  1. 由于依托于屏幕刷新频率，若果CPU不堪重负而影响了屏幕刷新，那么我们的触发事件也会受到相应影响。\n  2. selector触发的时间间隔只能是duration的整倍数\n  3. selector事件如果大于其触发间隔就会造成掉帧现象。\n\n### dispatch_source_t\n\n```objective-c\n- (void)createGCDTimer {\n    \u002F\u002F\u002F 创建定时器\n    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());\n    \u002F\u002F\u002F 设置触发间隔时间\n    dispatch_source_set_timer(self.timer, dispatch_walltime(NULL,0 * NSEC_PER_SEC), 1 * NSEC_PER_SEC, 0);\n    \n  \t\u002F\u002F\u002F 设置回调\n    dispatch_source_set_event_handler(self.timer, ^{\n        NSLog(@\"triggered\");\n    });\n}\n```\n\n**参数：**\n\n**dispatch_source_create()相关参数**\n\n|  参数  | 意义                                                       |\n| :----: | ---------------------------------------------------------- |\n|  type  | dispatch源可处理的事件                                     |\n| handle | 可以理解为句柄、索引或id，假如要监听进程，需要传入进程的ID |\n|  mask  | 可以理解为描述，提供更详细的描述，让它知道具体要监听什么   |\n| queue  | 自定义源需要的一个队列，用来处理所有的响应句柄（block）    |\n\n**dispatch_source_set_timer()相关参数**\n\n|   参数   | 意义                   |\n| :------: | ---------------------- |\n|  source  | dispatch_source_t      |\n|  start   | 事件首次触发的延迟时间 |\n| interval | 时间间隔               |\n|  leeway  | 误差范围               |\n\n#### 开始 暂停\n\n```objectivec\n\u002F\u002F开始\ndispatch_resume(self.disTimer);\n\u002F\u002F暂停\ndispatch_suspend(self.disTimer);\n```\n\n#### 销毁\n\n```css\ndispatch_source_cancel(self.disTimer);\n```\n\n**注意：**dispatch_source_t 一定要被设置为成员变量，否则将会立即被释放。\n\n#### 优缺点\n\n- **优点：**不受当前runloopMode的影响;\n- **缺点：**虽然不受runloopMode的影响，但是其计时效应仍不是百分之百准确的;\n\n# 六、视图&图像相关\n\n## 1. AutoLayout的原理，性能如何\n\n参考文章：[AutoLayout 的原理性能](https:\u002F\u002Fwww.dazhuanlan.com\u002F2019\u002F10\u002F05\u002F5d97e8f15427d\u002F)\n\n## 2. UIView & CALayer的区别\n\n* UIView 为 CALayer 提供内容，以及负责处理触摸等事件，参与响应链；\n\n* CALayer 负责显示内容 contents\n* 单一职责原则\n\n## 3. 事件响应链\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_c4758bef1002.png)\n\n>  慕尚课程的总结图\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_b4019fc64410.png)\n\n\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_b532e821caf4.png)\n\n## drawrect & layoutsubviews调用时机\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_74938b52bf6b.png)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_2d84d849c004.png)\n\n\n\n### layoutSubviews\n\n>  参考文章：[layoutSubviews和drawRect调用时机的探究](https:\u002F\u002Fblog.csdn.net\u002Fwangyanchang21\u002Farticle\u002Fdetails\u002F50774522)\n\n1. init初始化不会触发layoutSubviews。\n2. addSubview会触发layoutSubviews。\n3. 改变一个UIView的Frame会触发layoutSubviews，当然前提是frame的值设置前后发生了变化。\n4. 滚动一个UIScrollView引发UIView的重新布局会触发layoutSubviews。\n5. 旋转Screen会触发父UIView上的layoutSubviews事件。\n6. 直接调用 setNeedsLayout 或者 layoutIfNeeded。\n\n* `setNeedsLayout`\n  标记为需要重新布局，异步调用`layoutIfNeeded`刷新布局，不立即刷新，在下一轮runloop结束前刷新，对于这一轮`runloop`之内的所有布局和UI上的更新只会刷新一次，`layoutSubviews`一定会被调用。\n\n* `layoutIfNeeded`\n  如果有需要刷新的标记，立即调用`layoutSubviews`进行布局（如果没有标记，不会调用`layoutSubviews`）。\n\n\n\n## 4. 隐式动画 & 显示动画区别\n\n> 解答出自 [iOS动画-CALayer隐式动画原理与特性](https:\u002F\u002Fcloud.tencent.com\u002Fdeveloper\u002Farticle\u002F1418000)。\n\n隐式动画，指我们可以在不设定任何动画类型的情况下，仅仅改变CALayer的一个可做动画的属性，就能实现动画效果。\n\n### 1. 事务\n\n**事务**，其实是Core Animation用来包含一系列属性动画集合的机制，通过指定事务来改变图层的可动画属性，这些变化都不是立刻发生变化的，而是在事务被提交的时候才启动一个动画过渡到新值。任何可以做动画的图层属性都会被添加到栈顶的事务。\n\n```objective-c\n\u002F\u002F1.动画属性的入栈\n+ (void)begin;\n\n\u002F\u002F2.动画属性出栈\n+ (void)commit;\n\n\u002F\u002F3.设置当前事务的动画时间\n+ (void)setAnimationDuration:(CFTimeInterval)dur;\n\n\u002F\u002F4.获取当前事务的动画时间\n+ (CFTimeInterval)animationDuration;\n\n\u002F\u002F5.在动画结束时提供一个完成的动作\n+ (void)setCompletionBlock:(nullable void (^)(void))block;\n```\n\n现在再来考虑隐式动画，其实是Core Animation在每个RunLoop周期中会自动开始一次新的事务，即使你不显式的使用[CATranscation begin]开始一次事务，任何在一次RunLoop运行时循环中属性的改变都会被集中起来，执行默认0.25秒的动画。\n\n通过事务来设置动画：\n\n```objective-c\n[CATransaction begin];  \u002F\u002F入栈\n\u002F\u002F1.设置动画执行时间\n[CATransaction setAnimationDuration:3];\n\u002F\u002F2.设置动画执行完毕后的操作：颜色渐变之后再旋转90度\n[CATransaction setCompletionBlock:^{\n  CGAffineTransform transform = self.colorLayer.affineTransform;\n  transform  = CGAffineTransformRotate(transform, M_PI_2);\n  self.colorLayer.affineTransform = transform;\n}];\n\nCGFloat red = arc4random() % 255 \u002F 255.0;\nCGFloat green = arc4random() % 255 \u002F 255.0;\nCGFloat blue = arc4random() % 255 \u002F 255.0;\nUIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];\n_colorLayer.backgroundColor = randomColor.CGColor;\n[CATransaction commit];  \u002F\u002F出栈\n```\n\n### 2. 图层行为\n\n我们上述的实验对象是一个独立图层，如果直接对UIView或者CALayer关联的图层layer改变动画属性，这样是没有隐式动画效果的，这说明虽然Core Animation对所有的CALayer动画属性设置了隐式动画，但UIView把它关联的图层的这个特性给关闭了。 为了更好的理解中一点，我们需要知道隐式动画是如何实现的： 我们把改变属性时CALayer自动执行的动画称作行为，当CALayer的属性被修改时，它会调用-actionForKey:方法传递属性名称，我们可以找到这个方法的具体说明如下：\n\n```objective-c\n\u002F* Returns the action object associated with the event named by the\n * string 'event'. The default implementation searches for an action\n * object in the following places:\n *\n * 1. if defined, call the delegate method -actionForLayer:forKey:\n * 2. look in the layer's `actions' dictionary\n * 3. look in any `actions' dictionaries in the `style' hierarchy\n * 4. call +defaultActionForKey: on the layer's class\n *\n * If any of these steps results in a non-nil action object, the\n * following steps are ignored. If the final result is an instance of\n * NSNull, it is converted to `nil'. *\u002F\n\n- (nullable id\u003CCAAction>)actionForKey:(NSString *)event;\n```\n\n翻译过来大概就是说：\n\n1. 图层会首先检测它是否有委托，并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法；如果有，就直接调用并返回结果。\n2. 如果没有委托或者委托没有实现-actionForLayer:forKey方法，图层将会检查包含属性名称对应行为映射的actions字典\n3. 如果actions字典没有包含对应的属性，图层接着在它的style字典里搜索属性名.\n4. 最后，如果在style也找不到对应的行为，那么图层将会直接调用定义了每个属性的标准行为的+defaultActionForKey:方法\n\n从流程上分析来看，经过一次完整的搜索动画之后，-actionForKey:要么返回空(这种情况不会有动画发生)，要么返回遵循CAAction协议的对象(CALayer拿这个结果去对先前和当前的值做动画)。现在我们再来考虑UIKit是如何禁用隐式动画的： 每个UIView对它关联的图层都遵循了CALayerDelegate协议，并且实现了-actionForLayer:forKey方法。当不在一个动画块中修改动画属性时，UIView对所有图层行为都返回了nil，但是在动画Block范围就返回了非空值，下面通过一段代码来验证：\n\n```objective-c\n@interface TestLayerAnimationVC ()\n\n@property (nonatomic,weak)IBOutlet UIView *layerView;\n\n@end\n\n- (void)viewDidLoad {\n    [super viewDidLoad];\n   \u002F\u002F测试图层行为：UIKit默认关闭了自身关联图层的隐式动画\n    NSLog(@\"OutSide:%@\",[self.layerView actionForLayer:self.layerView.layer forKey:@\"backgroundColor\"]);\n   \n    [UIView beginAnimations:nil context:nil];\n    NSLog(@\"InSide:%@\",[self.layerView actionForLayer:self.layerView.layer forKey:@\"backgroundColor\"]);\n    [UIView commitAnimations];\n}\n\n\u002F\u002F打印：\nOutSide:\u003Cnull>\nInSide:\u003CCABasicAnimation: 0x600001703100>\n```\n\n由此得出结论：当属性在动画块之外发生变化，UIView直接通过返回nil来禁用隐式动画。但是如果在动画块范围内，UIView则会根据动画具体类型返回响应的属性，\n\n### 3. 关闭和开启隐式动画\n\n当然，返回nil并不是禁用隐式动画的唯一方法，CATransaction也为我们提供了具体的方法，可以用来对所有属性打开或者关闭隐式动画，方法如下：\n\n```javascript\n+ (void)setDisableActions:(BOOL)flag;\n```\n\nUIView关联的图层禁用了隐式动画，那么对这种图层做动画的方法有有了以下几种方式：\n\n1. 使用UIView的动画函数(而不是依赖CATransaction)\n2. 继承UIView，并覆盖-actionforLayer:forkey:方法\n3. 直接创建显式动画\n\n其实，对于单独存在的图层，我们也可以通过实现图层的-actionforLayer:forkey:方法，或者提供一个actions字典来控制隐式动画\n\n### 4. 自定义图层行为\n\n通过对事务和图层行为的了解，我们可以这样思考，图层行为其实是被Core Animation隐式调用的显式动画对象。我们可以发现改变隐式动画的这种图层行为有两种方式： 1.给layer设置自定义的actions字典 2.实现委托代理，返回遵循CAAction协议的动画对象 现在，我们尝试使用第一种方法来自定义图层行为，这里用到的是一个推进过渡的动画(也是遵循了CAAction的动画类)，具体的代码如下：\n\n```javascript\n@interface TestLayerAnimationVC ()\n@property (nonatomic,strong) CALayer *colorLayer;\n@end\n\n- (void)viewDidLoad {\n    [super viewDidLoad];\n\n    _colorLayer = [[CALayer alloc] init];\n    _colorLayer.frame = CGRectMake(30, 30, kDeviceWidth - 60, 60);\n    _colorLayer.backgroundColor = [UIColor orangeColor].CGColor;\n    \u002F\u002F自定义动画对象\n    CATransition *transition = [CATransition animation];\n    transition.type = kCATransitionPush;\n    transition.subtype = kCATransitionFromLeft;\n    _colorLayer.actions = @{@\"backgroundColor\":transition};\n    [self.view.layer addSublayer:_colorLayer];\n}\n\n- (IBAction)changeColor:(UIButton *)sender{\n    CGFloat red = arc4random() % 255 \u002F 255.0;\n    CGFloat green = arc4random() % 255 \u002F 255.0;\n    CGFloat blue = arc4random() % 255 \u002F 255.0;\n    UIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];\n    _colorLayer.backgroundColor = randomColor.CGColor;\n}\n```\n\n\n\n## 5. 什么是离屏渲染\n\n[离屏渲染专题](https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record\u002Ftree\u002Fmaster\u002Ftopics\u002F离屏渲染专题)\n\n## 6. imageNamed & imageWithContentsOfFile区别\n\niOS加载本地图片有两种方式：imageNamed 和 imageWithContentOfFile 两种：\n\n#### imageNamed\n\n前者Apple官方文档有说到：\n\n> This method looks in the system caches for an image object with the specified name and returns that object if it exists. If a matching image object is not already in the cache, this method locates and loads the image data from disk or asset catelog, and then returns the resulting object. You can not assume that this method is thread safe.\n\n首先从系统缓存中根据图片名称寻找图片，如果找到了就返回。如果没有在缓存中找到图片，该方法会从指定的文件中加载图片数据，并将其缓存起来，然后再把结果返回，下次再使用该名称图片的时候就省去了从硬盘中加载图片的过程。对于相同名称的图片，**系统只会把它Cache到内存一次，**如果相应的图片数据不存在，返回nil。\n\n关于加载：`imageNamed` 方法可以加载 `Assets.xcassets` 和 bundle 中的图片。\n\n关于缓存：加载到内存当中会一直存在内存当中，（图片）不会随着对象的销毁而销毁；加载进去图片后，占用的内存归系统管理，我们是无法管理的；相同的图片是不会重复加载的，加载到内存中占据的内存较大。\n\n```objective-c\nUIImage *img = [UIImage imageNamed:@\"pic\"];\n```\n\n#### imageWithContentsOfFile\n\n`imageWithContentsOfFile` 方法只是简单的加载图片，并不会将图片缓存起来，图像会被系统以数据方式加载到程序。\n\n关于加载：`imageWithContentsOfFile` 只能加载 mainBundle 中图片。\n\n关于缓存：加载到内存中占据的内存较小，相同的图片会被重复加载到内存当中，加载的图片会随着对象的销毁而销毁；\n\n#### 两者应用场景\n\n* 如果图片较小，并且使用频繁的图片使用 imageNamed：方法来加载\n* 如果图片较大，并且使用较少，使用imageWithContentOfFile:来加载。\n* 当你不需要重用该图像，或者你需要将图像以数据方式存储到数据库，又或者你要通过网络下载一个很大的图像时，使用 `imageWithContentsOfFile`；\n* 如果在程序中经常需要重用的图片，比如用于UITableView的图片，那么最好是选择imageNamed方法。这种方法可以节省出每次都从磁盘加载图片的时间；\n\n> *  [《iOS之图片加载》](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002Fea2a2ba3cd97)\n> *  [IOS如何选择图片加载方式：imageNamed和imageWithContentsOfFile的区别](https:\u002F\u002Fblog.csdn.net\u002Fwzzvictory\u002Farticle\u002Fdetails\u002F9053813)\n\n## 7. 多个相同的图片，会重复加载吗\n\n答案见上\n\n## 8. 图片是什么时候解码的，如何优化\n\n* [iOS 图片解码（decode）笔记](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F4da6981a746c)\n* [探讨iOS 中图片的解压缩到渲染过程](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F72dd074728d8)\n\n## 9. 图片渲染怎么优化\n\n> 出自 swift.gg 文章 [图像渲染优化技巧](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F)\n\n####9.1 图片尺寸**明显大于** **`UIImageView`** 显示尺寸的场景\n\n1. [绘制到 UIGraphicsImageRenderer 上](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-1-drawing-to-a-uigraphicsimagerenderer)\n\n   ```swift\n   import UIKit\n   \n   \u002F\u002F 技巧 #1\n   func resizedImage(at url: URL, for size: CGSize) -> UIImage? {\n       guard let image = UIImage(contentsOfFile: url.path) else {\n           return nil\n       }\n   \n       let renderer = UIGraphicsImageRenderer(size: size)\n       return renderer.image { (context) in\n           image.draw(in: CGRect(origin: .zero, size: size))\n       }\n   }\n   ```\n\n   [`UIGraphicsImageRenderer`](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fuikit\u002Fuigraphicsimagerenderer) 是一项相对较新的技术，在 iOS 10 中被引入，用以取代旧版本的 `UIGraphicsBeginImageContextWithOptions` \u002F `UIGraphicsEndImageContext` API。你通过指定以 `point` 计量的 `size` 创建了一个 `UIGraphicsImageRenderer`。`image` 方法带有一个闭包参数，返回的是一个经过闭包处理后的位图。最终，原始图像便会在缩小到指定的范围内绘制。\n\n   > 在不改变图像原始纵横比（aspect ratio）的情况下，缩小图像原始的尺寸来显示通常很有用。[`AVMakeRect(aspectRatio:insideRect:)`](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Favfoundation\u002F1390116-avmakerect) 是在 AVFoundation 框架中很方便的一个函数，负责帮你做如下的计算：\n\n   ```\n   import func AVFoundation.AVMakeRect\n   let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)\n   ```\n\n2. [绘制到 Core Graphics Context 上](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-2-drawing-to-a-core-graphics-context)\n\n   ```swift\n   import UIKit\n   import CoreGraphics\n   \n   \u002F\u002F 技巧 #2\n   func resizedImage(at url: URL, for size: CGSize) -> UIImage? {\n       guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),\n           let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)\n       else {\n           return nil\n       }\n   \n       let context = CGContext(data: nil,\n                               width: Int(size.width),\n                               height: Int(size.height),\n                               bitsPerComponent: image.bitsPerComponent,\n                               bytesPerRow: image.bytesPerRow,\n                               space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,\n                               bitmapInfo: image.bitmapInfo.rawValue)\n       context?.interpolationQuality = .high\n       context?.draw(image, in: CGRect(origin: .zero, size: size))\n   \n       guard let scaledImage = context?.makeImage() else { return nil }\n   \n       return UIImage(cgImage: scaledImage)\n   }\n   ```\n\n   \n\n3. [使用 Image I\u002FO 创建缩略图像](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-3-creating-a-thumbnail-with-image-io)\n\n   Image I\u002FO 是一个强大（却鲜有人知）的图像处理框架。抛开 Core Graphics 不说，它可以读写许多不同图像格式，访问图像的元数据，还有执行常规的图像处理操作。这个框架通过先进的缓存机制，提供了平台上最快的图片编码器和解码器，甚至可以增量加载图片。\n\n   这个重要的 `CGImageSourceCreateThumbnailAtIndex` 提供了一个带有许多不同配置选项的 API，比起在 Core Graphics 中等价的处理操作要简洁得多：\n\n4. [使用 Core Image 进行 Lanczos 重采样](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-4-lanczos-resampling-with-core-image)\n\n   Core Image 内置了 [Lanczos 重采样（resampling）](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLanczos_resampling) 功能，它是以 `CILanczosScaleTransform` 的同名滤镜命名的。虽然可以说它是在 UIKit 层级之上的 API，但无处不在的 key-value 编写方式导致它使用起来很不方便。\n\n   即便如此，它的处理模式还是一致的。\n\n   创建转换滤镜，对滤镜进行配置，最后渲染输出图像，这样的步骤和其他任何 Core Image 的工作流没什么不同。\n\n   ```\n   import UIKit\n   import CoreImage\n   \n   let sharedContext = CIContext(options: [.useSoftwareRenderer : false])\n   \n   \u002F\u002F 技巧 #4\n   func resizedImage(at url: URL, scale: CGFloat, aspectRatio: CGFloat) -> UIImage? {\n       guard let image = CIImage(contentsOf: url) else {\n           return nil\n       }\n   \n       let filter = CIFilter(name: \"CILanczosScaleTransform\")\n       filter?.setValue(image, forKey: kCIInputImageKey)\n       filter?.setValue(scale, forKey: kCIInputScaleKey)\n       filter?.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)\n   \n       guard let outputCIImage = filter?.outputImage,\n           let outputCGImage = sharedContext.createCGImage(outputCIImage,\n                                                           from: outputCIImage.extent)\n       else {\n           return nil\n       }\n   \n       return UIImage(cgImage: outputCGImage)\n   }\n   ```\n\n5. [使用 vImage 优化图片渲染](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-5-image-scaling-with-vimage)\n\n   最后一个了，它是古老的 [Accelerate 框架](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Faccelerate) —— 更具体点来说，它是 `vImage` 的图像处理子框架。\n\n   vImage 附带有 [一些不同的功能](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Faccelerate\u002Fvimage\u002Fvimage_operations\u002Fimage_scaling)，可以用来裁剪图像缓冲区大小。这些底层 API 保证了高性能同时低能耗，但会导致你对缓冲区的管理操作增加（更不用说要编写更多的代码了）\n\n- **UIKit**, **Core** **Graphics**, 和 **Image** **I\u002FO** 都能很好地用于大部分图片的优化操作。如果（在 iOS 平台，至少）要选择一个的话，`UIGraphicsImageRenderer` 是你最佳的选择。\n- **Core** **Image** 在图像优化渲染操作方面性能表现优越。实际上，根据 Apple 官方 [*Core* *Image* *编程规范中的性能最佳实践单元*](https:\u002F\u002Fdeveloper.apple.com\u002Flibrary\u002Fmac\u002Fdocumentation\u002Fgraphicsimaging\u002FConceptual\u002FCoreImaging\u002Fci_performance\u002Fci_performance.html#\u002F\u002Fapple_ref\u002Fdoc\u002Fuid\u002FTP30001185-CH10-SW1)，你应该使用 Core Graphics 或 Image I\u002FO 对图像进行裁剪和下采样，而不是用 Core Image。\n\n## 10. 如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象，怎么解决\n\n# 七、性能优化\n\n1. 如何做启动优化，如何监控\n2. 如何做卡顿优化，如何监控\n   1. FPS 用 CADisplayLinker 来计数\n   2. 监听 runloop 的 source0 事件和进入休眠前，然后设定一个阈值，超过几次算卡顿\n   3. ping 方案，起一个子线程，while(1) 每次 async 一个 task 到主线程进行标志位置位，然后休眠或者等待一定时间在子线程检查是否这个task被执行了。\n3. 如何做耗电优化，如何监控\n4. 如何做网络优化，如何监控\n\n> [iOS面试题：简述性能优化](https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F96963676)\n\n# 八、开发证书\n\n1. 苹果使用证书的目的是什么\n\n   在 iOS 平台对第三方 APP 有绝对的控制权，一定要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的，场景有如下三种\n\n   1. AppStore 下载应用验证，传 App 上 AppStore 时，苹果后台用私钥对 APP 数据进行签名，iOS 系统下载这个 APP 后，用公钥验证这个签名，若签名正确，这个 APP 肯定是由苹果后台认证的，并且没有被修改过，也就达到了苹果的需求：保证安装的每一个 APP 都是经过苹果官方允许的。\n   2. 开发 App 时可以直接把开发中的应用安装进手机进行调试。\n   3. In-House 企业内部分发，可以直接安装企业证书签名后的 APP。\n   4. AD-Hoc 相当于企业分发的限制版，限制安装设备数量，较少用。\n\n2. AppStore安装app时的认证流程\n\n   略\n\n3. 开发者怎么在debug模式下把app安装到设备呢\n\n   略\n\n# 九、架构设计\n\n## 典型源码的学习\n\n只是列出一些iOS比较核心的开源库，这些库包含了很多高质量的思想，源码学习的时候一定要关注每个框架解决的核心问题是什么，还有它们的优缺点，这样才能算真正理解和吸收\n\n1. AFN\n2. SDWebImage\n3. JSPatch、Aspects(虽然一个不可用、另一个不维护，但是这两个库都很精炼巧妙，很适合学习)\n4. Weex\u002FRN, 笔者认为这种前端和客户端紧密联系的库是必须要知道其原理的\n5. CTMediator、其他router库，这些都是常见的路由库，开发中基本上都会用到\n6. 请`圈友`们在评论下面补充吧\n\n## 架构设计\n\n1. 手动埋点、自动化埋点、可视化埋点\n2. `MVC、MVP、MVVM`设计模式\n3. 常见的设计模式\n4. 单例的弊端\n5. 常见的路由方案，以及优缺点对比\n6. 如果保证项目的稳定性\n7. 设计一个图片缓存框架(LRU)\n8. 如何设计一个`git diff`\n9. 设计一个线程池？画出你的架构图\n10. 你的app架构是什么，有什么优缺点、为什么这么做、怎么改进\n\n# 十、其他问题\n\n1. `PerformSelector & NSInvocation`优劣对比\n2. `oc`怎么实现多继承？怎么面向切面（可以参考[Aspects深度解析-iOS面向切面编程](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5e13c4366fb9a047f42e6406)）\n3. 哪些`bug`会导致崩溃，如何防护崩溃\n4. 怎么监控崩溃\n5. `app`的启动过程（考察LLVM编译过程、静态链接、动态链接、runtime初始化）\n6. 沙盒目录的每个文件夹划分的作用\n7. 简述下`match-o`文件结构\n\n# 十一、系统基础知识\n\n### 进程和线程的区别\n\n> 这个解答很多，暂时引自[进程和线程的区别介绍](https:\u002F\u002Fbaijiahao.baidu.com\u002Fs?id=1611925141861592999&wfr=spider&for=pc) 一文。\n\n1、首先是定义\n\n进程：是执行中一段程序，即一旦程序被载入到内存中并准备执行，它就是一个进程。进程是表示资源分配的的基本概念，又是调度运行的基本单位，是系统中的并发执行的单位。\n\n线程：单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。\n\n2、一个线程只能属于一个进程，但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。\n\n3、线程是一种轻量级的进程，与进程相比，线程给操作系统带来侧创建、维护、和管理的负担要轻，意味着线程的代价或开销比较小。\n\n4、线程没有地址空间，线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权，线程文本包含在他的进程 的文本片段中，进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量)，数据段(全局变量和静态变量)，扩展段(堆存储)。但是每个线程拥有自己的栈段， 寄存器的内容，栈段又叫运行时段，用来存放所有局部变量和临时变量。\n\n5、父和子进程使用进程间通信机制，同一进程的线程通过读取和写入数据到进程变量来通信。\n\n6、进程内的任何线程都被看做是同位体，且处于相同的级别。不管是哪个线程创建了哪一个线程，进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制，进程中任何线程都可以通过销毁主线程来销毁进程，销毁主线程将导致该进程的销毁，对主线程的修改可能影响所有的线程。\n\n7、子进程不对任何其他子进程施加控制，进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制，进程中所有线程都可以对主线程施加控制。\n\n相同点：\n\n进程和线程都有ID\u002F寄存器组、状态和优先权、信息块，创建后都可更改自己的属性，都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。\n\n### `HTTPS`的握手过程\n\n密钥协商过程：\n\n1. 客户端将TLS版本，支持的加密算法，ClientHello random C 发给服务端【客户端->服务端】\n2. 服务端从加密算法中pick一个加密算法， ServerHello random  S，server 证书返回给客户端；【服务端->客户单】\n3. 客户端验证 server 证书【客户端】\n4. 客户端生成一个 48 字节的预备主密钥，其中前2个字节是 Protocol Version，后46个字节是随机数，客户端用证书中的公钥对预备主密钥进行非对称加密后通过 client key exchange 子消息发给服务端【客户端->服务端】\n5. 服务端用私钥解密得到预备主密钥；【服务端】\n6. 服务端和客户端都可以通过预备主密钥、ClientHello random C 和  ServerHello random S 通过 PRF 函数生成主密钥；会话密钥由主密钥、SecurityParameters.server_random 和 SecurityParameters.client_random 数通过 PRF 函数来生成会话密钥里面包含对称加密密钥、消息认证和 CBC 模式的初始化向量，对于非 CBC 模式的加密算法来说，就没有用到这个初始化向量。\n\n> Session ID 缓存和 Session Ticket 里面保存的也是主密钥，而不是会话密钥，这样每次会话复用的时候再用双方的随机数和主密钥导出会话密钥，从而实现每次加密通信的会话密钥不一样，即使一个会话的主密钥泄露了或者被破解了也不会影响到另一个会话。\n\n### 什么是`中间人攻击`？怎么预防\n\nHTTP 明文传输，客户端和服务端进行通信时，中间人即指夹在客户端和服务端之间的第三者，对于客户端来说，中间人就是 **服务端**，对于服务端来说，中间人就是 **客户端**。中间人拦截客户端消息，然后再发送给服务端；服务端发发送消息给中间人，中间人再返还给客户端。\n\n使用 HTTPS，单双向认证，\n\n### `TCP`的握手过程？为什么进行三次握手，四次挥手\n\n三次握手：\n\n> 为了确认服务端和客户端双方的收发能力。\n\n* 客户端发送 SYN = 1，seq=x 给服务端\n* 服务端接收发送 SYN = 1，ACK = 1，ack=x+1， seq = y 给客户端\n* 客户端发送 ACK = 1，ack = y+1 ，seq = z 给服务端\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_6bc22c1f6c61.png)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_87b5ca5299f7.png)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_54b291f831ea.png)\n\n四次挥手：\n\n* 主动方发送 FIN = 1，seq = u 给被动方；\n* 被动方 ACK = 1，ack = u+1，seq = v；\n* 被动方继续传输数据给主动方；\n* 被动方没有更多数据了，发送 FIN = 1，ACK=1，seq = w，ack=u+1；\n* 主动方 ACK = 1，seq = u + 1，ack = w +1;\n\n### `堆和栈`区的区别？谁的占用内存空间大\n\n> 解答出自：[堆区（heap）和栈区（stack）的区别](https:\u002F\u002Fblog.csdn.net\u002Fshanshanhi\u002Farticle\u002Fdetails\u002F50904706)\n\n（1）申请方式\n\n栈区：由编译器自动分配释放，存放函数的参数值，局部变量值等；\n\n堆区：一般由程序员分配释放（使用new\u002Fdelete或malloc\u002Ffree），若程序员不释放，程序结束时可能由OS回收；\n\n（2）操作方式\n\n栈区：操作方式类似于数据结构中的栈；\n\n堆区：不同于数据结构中的堆，分配方式类似于链表。\n\n（3）申请后系统的响应 \n\n栈区：只要栈的剩余空间大于所申请空间，系统将为程序提供内存，否则将报异常提示栈溢出；\n\n堆区：首先应该知道操作系统有一个记录空闲内存地址的链表，当系统收到程序的申请时，会遍历该链表，寻找第一个空间大于所申请空间的堆结点，然后将该结点从空闲结点链表中删除，并将该结点的空间分配给程序，另外，对于大多数系统，会在这块内存空间中的首地址处记录本次分配的大小，这样，代码中的delete语句才能正确的释放本内存空间。另外，由于找到的堆结点的大小不一定正好等于申请的大小，系统会自动的将多余的那部分重新放入空闲链表中。 \n\n（4）申请大小的限制\n\n栈区：在Windows下,栈是向低地址扩展的数据结构，是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的，在WINDOWS下，栈的大小是2M（也有的说是1M，总之是一个编译时就确定的常数），如果申请的空间超过栈的剩余空间时，将提示overflow。因此，能从栈获得的空间较小。\n\n堆区：堆是向高地址扩展的数据结构，是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的，自然是不连续的，而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见，堆获得的空间比较灵活，也比较大。 \n\n（5）申请效率的比较\n\n栈区：系统自动分配，速度较快。但程序员是无法控制的。\n\n堆区：由new分配的内存，一般速度比较慢，而且容易产生内存碎片,不过用起来最方便. \n\n注意：在WINDOWS下，最好的方式是用VirtualAlloc分配内存，他不是在堆，也不是在栈是直接在进程的地址空间中保留一快内存，虽然用起来最不方便。但是速度快，也最灵活。\n\n（6）堆和栈中的存储内容\n\n栈区：在函数调用时，第一个进栈的是主函数中后的下一条指令（函数调用语句的下一条可执行语句）的地址，然后是函数的各个参数，在大多数的C编译器中，参数是由右往左入栈的，然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后，局部变量先出栈，然后是参数，最后栈顶指针指向最开始存的地址，也就是主函数中的下一条指令，程序由该点继续运行。\n\n堆区：一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。\n\n### 加密算法：`对称加密算法和非对称加密算法`区别\n\n### 常见的`对称加密和非对称加密`算法有哪些\n\n#### 对称加密\n\n对称加密指的就是加密和解密使用同一个秘钥，所以叫做对称加密。对称加密只有一个秘钥，作为私钥。\n\n具体算法有：DES，3DES，TDEA，Blowfish，RC5，IDEA。常见的有：DES，AES，3DES等等。\n\n> 优点：算法公开、计算量小、加密速度快、加密效率高。 缺点：秘钥的管理和分发非常困难，不够安全。在数据传送前，发送方和接收方必须商定好秘钥，然后双方都必须要保存好秘钥，如果一方的秘钥被泄露，那么加密信息也就不安全了。另外，每对用户每次使用对称加密算法时，都需要使用其他人不知道的唯一秘钥，这会使得收、发双方所拥有的钥匙数量巨大，密钥管理成为双方的负担。\n\n#### 非对称加密\n\n非对称加密指的是：加密和解密使用不同的秘钥，一把作为公开的公钥，另一把作为私钥。公钥加密的信息，只有私钥才能解密。私钥加密的信息，只有公钥才能解密。 私钥只能由一方安全保管，不能外泄，而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密，而解密则需要另一个密钥。\n\n我们常见的数字证书、加密狗即是采用非对称加密来完成安全验证的。\n\n> 优点：安全性更高，公钥是公开的，秘钥是自己保存的，不需要将私钥给别人。 缺点：加密和解密花费时间长、速度慢，只适合对少量数据进行加密。\n\n主要算法：RSA、Elgamal、背包算法、Rabin、HD,ECC（椭圆曲线加密算法）。常见的有：RSA，ECC\n\n#### 银行动态令牌\n\n网银比较流行的时候，银行给我们发一个动态令牌。这个令牌并不使用任何对称或者非对称加密的算法，在整个银行的认证体系中，动态令牌只是一个一次性口令的产生器，它是基于时间同步方式，每隔60秒产生一个随机6位动态密码在其中运行的主要计算仅包括时间因子的计算和散列值的计算。\n\n在用户从银行手中拿到动态口令令牌卡的时候，在令牌卡的内部已经存储了一份种子文件（即图中钥匙所代表的seed），这份种子文件在银行的服务器里保存的完全一样的一份，所以对于动态口令令牌来说，这种方式是share secret的。另外在令牌硬件上的设置中，假使有人打开了这个令牌卡，种子文件将会从令牌卡的内存上擦除（待考证）。 令牌卡中有了种子文件，并实现了TOTP算法，在预先设置的间隔时间里它就能不断产生不同的动态口令，并显示到屏幕上，而银行服务器上跟随时间做同样的计算，也会得到和令牌卡同样的口令，用作认证。 那么TOTP算法具体做了什么操作呢？在RFC6238中有详细的算法描述，这里也会做简单的叙述。\n\n> TOTP是来自 HOTP [RFC4226] 的变形，从统筹上看，他们都是将数据文件进行散列计算，只是HOTP的因子是事件因子，TOTP将因子换成了时间因子，具体的TOTP计算公式(其中的HMAC-SHA-256也可能是 HMAC-SHA-512)： TOTP = Truncate(HMAC-SHA-256(K,T))\n\n其中： K 为这里的种子文件内容； T 为计算出来的时间因子 公式中的 HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)，HMAC运算利用哈希算法，以一个密钥和一个消息为输入，生成一个消息摘要作为输出。而公式中给出的哈希算法是 SHA-256，这种哈希算法目前并没有好的破解办法。 令牌卡中预先设置了要显示的口令长度，TOTP 中的 Truncate 操作剪切获得口令。 以上就是动态口令令牌卡的内部原理。\n\n> 解答出自 [对称加密算法与非对称加密算法的优缺点](https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F38307899)\n\n### `MD5、Sha1、Sha256`区别\n\n签名算法，SHA(Security Hash Algorithm) ，貌似 MD5 更高效，花费时间更少，但相对较容易**碰撞**。SHA1 已经被攻破，所以安全性不行。\n\n### `charles`抓包过程？不使用`charles`，`4G`网络如何抓包\n\n 中间人攻击原理，easy。\n\n# 十二、数据结构与算法\n\n>  pmst： 建议 [LeetCode](https:\u002F\u002Fleetcode.com\u002F)上刷题，面试是一方面，锻炼我们的*逻辑*思维、分析能力，就比如今天的每日一题[面试题 16.03. 交点](https:\u002F\u002Fleetcode-cn.com\u002Fproblems\u002Fintersection-lcci\u002F)，问题难度不高，就是初中的斜线问题，主要是分析多种情况下的交点，理顺逻辑很重要。\n\n1. 八大排序算法\n2. 栈&队列\n3. 字符串处理\n4. 链表\n5. 二叉树相关操作\n6. 深搜广搜\n7. 基本的动态规划题、贪心算法、二分查找\n\n> 如果你觉得这份资料对你有价值，欢迎 star，fork，☕️\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_c06580f5f0cf.png)\n\n","# 阿里、字节 一套高效的iOS面试题解答\n> - 面试题出自掘金的一篇文章[《阿里、字节：一套高效的iOS面试题》](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=https%3A%2F%2Fjuejin.im%2Fpost%2F5e397ccaf265da570b3f1b02)\n> - 欢迎转载，转载请注明出处：[pmst-swiftgg](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=%5Bhttps%3A%2F%2Fwww.jianshu.com%2Fp%2Fc1765a6305ab%5D(https%3A%2F%2Fwww.jianshu.com%2Fp%2Fc1765a6305ab))\n> - 调试好可运行的源码 [objc-runtime](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=https%3A%2F%2Fgithub.com%2Fcolourful987%2F2020-Read-Record%2Ftree%2Fmaster%2FAnnotated%20source%20code%2Fobjc4-750)，官网找 [objc4](https:\u002F\u002Flinks.jianshu.com\u002Fgo?to=https%3A%2F%2Fopensource.apple.com%2Ftarballs%2Fobjc4%2F)\n> - 关于内推：目前博主就职于字节跳动，可帮忙内推，关于面试也可解答一二，欢迎发邮件联系或投递简历，我会第一时间处理，Email: mcxcode@163.com\n> - 最后修订：2020\u002F05\u002F11\n\n[TOC]\n\n# 一、runtime相关问题\n\n> * 调试好可运行的源码 [objc-runtime](https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record\u002Ftree\u002Fmaster\u002FAnnotated%20source%20code\u002Fobjc4-750)，官网找 [objc4](https:\u002F\u002Fopensource.apple.com\u002Ftarballs\u002Fobjc4\u002F)；\n>\n\n## 结构模型\n\n### 1. 介绍下runtime的内存模型（isa、对象、类、metaclass、结构体的存储信息等）\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_1d68fef9a20e.png)\n\n\n\n### 2. 为什么要设计metaclass\n\n* 类对象、元类对象能够复用消息发送流程机制；\n* 单一职责原则\n\n### 3. `class_copyIvarList` & `class_copyPropertyList`区别\n\n`property` 正常使用会生成对应的实例变量，所以 `Ivar` 可以查到。\n\n`class_copyIvarList` 获取类对象中的所有实例变量信息，从 `class_ro_t` 中获取：\n\n```objective-c\nIvar *\nclass_copyIvarList(Class cls, unsigned int *outCount)\n{\n    const ivar_list_t *ivars;\n    Ivar *result = nil;\n    unsigned int count = 0;\n\n    if (!cls) {\n        if (outCount) *outCount = 0;\n        return nil;\n    }\n\n    mutex_locker_t lock(runtimeLock);\n\n    assert(cls->isRealized());\n    \n    if ((ivars = cls->data()->ro->ivars)  &&  ivars->count) {\n        result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar));\n        \n        for (auto& ivar : *ivars) {\n            if (!ivar.offset) continue;  \u002F\u002F anonymous bitfield\n            result[count++] = &ivar;\n        }\n        result[count] = nil;\n    }\n    \n    if (outCount) *outCount = count;\n    return result;\n}\n```\n\n`class_copyPropertyList` 获取类对象中的属性信息， `class_rw_t` 的 `properties`，先后输出了 category \u002F extension\u002F baseClass 的属性，而且仅输出当前的类的属性信息，而不会向上去找 superClass 中定义的属性。\n\n```objective-c\nobjc_property_t *\nclass_copyPropertyList(Class cls, unsigned int *outCount)\n{\n    if (!cls) {\n        if (outCount) *outCount = 0;\n        return nil;\n    }\n\n    mutex_locker_t lock(runtimeLock);\n\n    checkIsKnownClass(cls);\n    assert(cls->isRealized());\n    \n    auto rw = cls->data();\n\n    property_t **result = nil;\n    unsigned int count = rw->properties.count();\n    if (count > 0) {\n        result = (property_t **)malloc((count + 1) * sizeof(property_t *));\n        \n        count = 0;\n        for (auto& prop : rw->properties) {\n            result[count++] = &prop;\n        }\n        result[count] = nil;\n    }\n\n    if (outCount) *outCount = count;\n    return (objc_property_t *)result;\n}\n```\n\n> Q1: `class_ro_t` 中的 `baseProperties` 呢？\n>\n> Q2: `class_rw_t` 中的 `properties` 包含了所有属性，那何时注入进去的呢？ 答案见 5.\n\n### 4. `class_rw_t` 和 `class_ro_t` 的区别\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_8b32d7ebec51.png)\n\n测试发现，`class_rw_t` 中的 `properties` 属性按顺序包含分类\u002F扩展\u002F基类中的属性。\n\n```objective-c\nstruct class_ro_t {\n    uint32_t flags;\n    uint32_t instanceStart;\n    uint32_t instanceSize;\n#ifdef __LP64__\n    uint32_t reserved;\n#endif\n\n    const uint8_t * ivarLayout;\n    \n    const char * name;\n    method_list_t * baseMethodList;\n    protocol_list_t * baseProtocols;\n    const ivar_list_t * ivars;\n\n    const uint8_t * weakIvarLayout;\n    property_list_t *baseProperties;\n\n    method_list_t *baseMethods() const {\n        return baseMethodList;\n    }\n};\n\nstruct class_rw_t {\n    \u002F\u002F Be warned that Symbolication knows the layout of this structure.\n    uint32_t flags;\n    uint32_t version;\n\n    const class_ro_t *ro;\n\n    method_array_t methods;\n    property_array_t properties;\n    protocol_array_t protocols;\n\n    Class firstSubclass;\n    Class nextSiblingClass;\n\n    char *demangledName;\n\n#if SUPPORT_INDEXED_ISA\n    uint32_t index;\n#endif\n}\n```\n\n### 5. `category`是如何被加载的，两个category的`load`方法的加载顺序，两个category的同名方法的加载顺序\n\n> `+load` 方法是在 images 加载时调用的。假设有一个 Person 类，其主类和所有分类的 `+load` 都会被调用，优先级是先调用主类，且如果主类有继承链，那么加载顺序还必须是基类的 `+load` ，接着是父类，最后是子类；category 的 `+load` 则是按照编译顺序来的，先编译的先调用，后编译的后调用，可在 Xcode  的 BuildPhase 中查看，[测试 Demo 可点击下载运行](.\u002Fdemos\u002FTestLoad方法)；\n>\n> 另外一个问题是 `initialize` 的加载顺序，其实是类第一次被使用到的时候会被调用，底层实现有个逻辑先判断父类是否被初始化过，没有则先调用父类，然后在调用当前类的 `initialize` 方法；试想一种情况，一个类 A 存在多个 category ，且 category中各自实现了 initialize 方法，这时候走的是 **消息发送流程**，也就说 initialize 方法只会调用一次，也就是最后编译的那个category中的 initialize 方法，验证demo见上；\n>\n> 再考虑一种情况：如果`+load` 方法中调用了其他类：比如 B 的某个方法，其实说白了就是走消息发送流程，由于 B 没有初始化过，则会调用其 initialize 方法，但此刻 B 的 +load 方法可能还没有被系统调用过。\n>\n> **小结：** 不管是 load 还是 initialize 方法都是 runtime 底层自动调用的，如果开发自己手动进行了 `[super load]` 或者 `[super initialize]` 方法，实际上是走消息发送流程，那么这里也涉及了一个调用流程，需要引起注意。\n\n`... -> realizeClass -> methodizeClass(用于Attach categories)-> attachCategories` 关键就是在 methodizeClass 方法实现中\n\n```objective-c\nstatic void methodizeClass(Class cls)\n{\n    runtimeLock.assertLocked();\n\n    bool isMeta = cls->isMetaClass();\n    auto rw = cls->data();\n    auto ro = rw->ro;\n  \t\n  \t\u002F\u002F =======================================\n\t\t\u002F\u002F 省略.....\n  \t\u002F\u002F =======================================\n  \n    property_list_t *proplist = ro->baseProperties;\n    if (proplist) {\n        rw->properties.attachLists(&proplist, 1);\n    }\n\n  \t\u002F\u002F =======================================\n\t\t\u002F\u002F 省略.....\n  \t\u002F\u002F =======================================\n    \n    \u002F\u002F Attach categories.\n    category_list *cats = unattachedCategoriesForClass(cls, true \u002F*realizing*\u002F);\n    attachCategories(cls, cats, false \u002F*don't flush caches*\u002F);\n\n  \t\u002F\u002F =======================================\n\t\t\u002F\u002F 省略.....\n  \t\u002F\u002F =======================================\n    \n    if (cats) free(cats);\n\n}\n```\n\n上面代码能确定 baseProperties 在前，category 在后，但决定顺序的是 `rw->properties.attachLists` 这个方法：\n\n```objective-c\nproperty_list_t *proplist = ro->baseProperties;\nif (proplist) {\n  rw->properties.attachLists(&proplist, 1);\n}\n\n\u002F\u002F\u002F category 被附加进去\nvoid attachLists(List* const * addedLists, uint32_t addedCount) {\n        if (addedCount == 0) return;\n\n        if (hasArray()) {\n            \u002F\u002F many lists -> many lists\n            uint32_t oldCount = array()->count;\n            uint32_t newCount = oldCount + addedCount;\n            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));\n            array()->count = newCount;\n            \n            \u002F\u002F 将旧内容移动偏移量 addedCount 然后将 addedLists copy 到起始位置\n          \t\u002F*\n          \t\tstruct array_t {\n        \t\t\t\tuint32_t count;\n        \t\t\t\tList* lists[0];\n    \t\t\t\t\t};\n          \t*\u002F\n            memmove(array()->lists + addedCount, array()->lists, \n                    oldCount * sizeof(array()->lists[0]));\n            memcpy(array()->lists, addedLists, \n                   addedCount * sizeof(array()->lists[0]));\n        }\n        else if (!list  &&  addedCount == 1) {\n            \u002F\u002F 0 lists -> 1 list\n            list = addedLists[0];\n        } \n        else {\n            \u002F\u002F 1 list -> many lists\n            List* oldList = list;\n            uint32_t oldCount = oldList ? 1 : 0;\n            uint32_t newCount = oldCount + addedCount;\n            setArray((array_t *)malloc(array_t::byteSize(newCount)));\n            array()->count = newCount;\n            if (oldList) array()->lists[addedCount] = oldList;\n            memcpy(array()->lists, addedLists, \n                   addedCount * sizeof(array()->lists[0]));\n        }\n    }\n```\n\n所以 category 的属性总是在前面的，baseClass的属性被往后偏移了。\n\n>  Q1：那么多个 category 的顺序呢？答案见6\n\n2020\u002F03\u002F18 补充下应用程序 image 镜像加载到内存中时， Category 解析的过程，注意下面的 `while(i--)` 这里倒叙将 category 中的协议 方法 属性添加到了 `rw = cls->data()` 中的 `methods\u002Fproperties\u002Fprotocols` 中。\n\n```objective-c\nstatic void \nattachCategories(Class cls, category_list *cats, bool flush_caches)\n{\n    if (!cats) return;\n    if (PrintReplacedMethods) printReplacements(cls, cats);\n\n    bool isMeta = cls->isMetaClass();\n\n    \u002F\u002F fixme rearrange to remove these intermediate allocations\n    method_list_t **mlists = (method_list_t **)\n        malloc(cats->count * sizeof(*mlists));\n    property_list_t **proplists = (property_list_t **)\n        malloc(cats->count * sizeof(*proplists));\n    protocol_list_t **protolists = (protocol_list_t **)\n        malloc(cats->count * sizeof(*protolists));\n\n    \u002F\u002F Count backwards through cats to get newest categories first\n    int mcount = 0;\n    int propcount = 0;\n    int protocount = 0;\n    int i = cats->count;\n    bool fromBundle = NO;\n    while (i--) {\n        auto& entry = cats->list[i];\n\n        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);\n        if (mlist) {\n            mlists[mcount++] = mlist;\n            fromBundle |= entry.hi->isBundle();\n        }\n\n        property_list_t *proplist = \n            entry.cat->propertiesForMeta(isMeta, entry.hi);\n        if (proplist) {\n            proplists[propcount++] = proplist;\n        }\n\n        protocol_list_t *protolist = entry.cat->protocols;\n        if (protolist) {\n            protolists[protocount++] = protolist;\n        }\n    }\n    auto rw = cls->data();\n\t\t\n  \t\u002F\u002F 注意下面的代码，上面采用倒叙遍历方式，所以后编译的 category 会先add到数组的前部\n    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);\n    rw->methods.attachLists(mlists, mcount);\n    free(mlists);\n    if (flush_caches  &&  mcount > 0) flushCaches(cls);\n\n    rw->properties.attachLists(proplists, propcount);\n    free(proplists);\n\n    rw->protocols.attachLists(protolists, protocount);\n    free(protolists);\n}\n```\n\n\n\n### 6. `category` & `extension`区别，能给NSObject添加Extension吗，结果如何\n\ncategory:\n\n* 运行时添加分类属性\u002F协议\u002F方法\n* 分类添加的方法会“覆盖”原类方法，因为方法查找的话是从头至尾，一旦查找到了就停止了\n* 同名分类方法谁生效取决于编译顺序，image 读取的信息是倒叙的，所以编译越靠后的越先读入\n* 名字相同的分类会引起编译报错；\n\nextension:\n\n* 编译时决议\n* 只以声明的形式存在，多数情况下就存在于 .m 文件中；\n* 不能为系统类添加扩展\n\n### 7. 消息转发机制，消息转发机制和其他语言的消息机制优劣对比\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_fca37514705f.png)\n\n### 8. 在方法调用的时候，`方法查询-> 动态解析-> 消息转发` 之前做了什么\n\nOC中的方法调用，编译后的代码最终都会转成 `objc_msgSend(id , SEL, ...)` 方法进行调用，这个方法第一个参数是一个消息接收者对象，runtime通过这个对象的isa指针找到这个对象的类对象，从类对象中的cache中查找(**哈希查找，bucket 桶实现**)是否存在SEL对应的IMP，若不存在，则会在 method_list中查找（二分查找或者顺序查找），如果还是没找到，则会到supper_class中查找，仍然没找到的话，就会调用_objc_msgForward(id, SEL, ...)进行消息转发。\n\n### 9. `IMP`、`SEL`、`Method`的区别和使用场景\n\n三者的定义如下：\n\n```objective-c\ntypedef struct method_t *Method;\n\nusing MethodListIMP = IMP;\n\nstruct method_t {\n    SEL name;\n    const char *types;\n    MethodListIMP imp;\n};\n```\n\n`Method` 同样是一个对象，封装了方法名和实现，关于 [Type Encodings](https:\u002F\u002Fdeveloper.apple.com\u002Flibrary\u002Farchive\u002Fdocumentation\u002FCocoa\u002FConceptual\u002FObjCRuntimeGuide\u002FArticles\u002FocrtTypeEncodings.html#\u002F\u002Fapple_ref\u002Fdoc\u002Fuid\u002FTP40008048-CH100-SW1)。\n\n| 代码               | 含义                                                      |\n| :----------------- | :----------------------------------------------------------- |\n| `c`                | 一个 `char`                                                     |\n| `i`                | 一个 `int`                                                     |\n| `s`                | 一个 `short`                                                    |\n| `l`                | 在 64 位程序中，`long l` 被视为 32 位量。 |\n| `q`                | 一个 `long long`                                                |\n| `C`                | 一个 `unsigned char`                                           |\n| `I`                | 一个 `unsigned int`                                            |\n| `S`                | 一个 `unsigned short`                                          |\n| `L`                | 一个 `unsigned long`                                           |\n| `Q`                | 一个 `unsigned long long`                                      |\n| `f`                | 一个 `float`                                                    |\n| `d`                | 一个 `double`                                                   |\n| `B`                | 一个 C++ 的 `bool` 或 C99 的 `_Bool`                                |\n| `v`                | 一个 `void`                                                     |\n| `*`                | 一个字符字符串（`char *`）                                |\n| `@`                | 一个对象（无论是静态类型还是 `id` 类型）           |\n| `#`                | 一个类对象（`Class`）                                     |\n| `:`                | 一个方法选择器（`SEL`）                                    |\n| [*数组类型*]     | 一个数组                                                     |\n| {*name=type...*}   | 一个结构体                                                  |\n| (*name*=*type...*) | 一个联合体                                                      |\n| `b`num             | 一个包含 *num* 位的位域                                    |\n| `^`type            | 一个指向 *type* 的指针                                          |\n| `?`                | 一个未知类型（除其他用途外，此代码还用于函数指针） |\n\n例如，`-(void)hello:(NSString *)name` 的编码就是 `v@:@`。\n\n### 10. `load` 和 `initialize` 方法有什么区别？在继承关系中它们有什么不同？\n\n`load` 方法的调用时机，而且只调用当前类本身，不会调用父类的 `+load` 方法：\n\n```objective-c\nvoid\nload_images(const char *path __unused, const struct mach_header *mh)\n{\n    \u002F\u002F 如果这里没有 +load 方法，则不加锁直接返回。\n    if (!hasLoadMethods((const headerType *)mh)) return;\n\n    recursive_mutex_locker_t lock(loadMethodLock);\n\n    \u002F\u002F 发现所有需要加载的方法\n    {\n        mutex_locker_t lock2(runtimeLock);\n        prepare_load_methods((const headerType *)mh);\n    }\n\n    \u002F\u002F 调用 +load 方法（无需 runtimeLock - 可重入）\n    call_load_methods();\n}\n\nvoid call_load_methods(void)\n{\n    static bool loading = NO;\n    bool more_categories;\n\n    loadMethodLock.assertLocked();\n\n    \u002F\u002F 对于可重入调用不做任何操作；最外层的调用会完成工作。\n    if (loading) return;\n    loading = YES;\n\n    void *pool = objc_autoreleasePoolPush();\n\n    do {\n        \u002F\u002F 1. 重复调用类的 +load 方法，直到没有更多方法为止\n        while (loadable_classes_used > 0) {\n            call_class_loads();\n        }\n\n        \u002F\u002F 2. 只调用一次分类的 +load 方法\n        more_categories = call_category_loads();\n\n        \u002F\u002F 3. 如果还有未尝试过的类或分类，则继续调用 +load 方法\n    } while (loadable_classes_used > 0  ||  more_categories);\n\n    objc_autoreleasePoolPop(pool);\n\n    loading = NO;\n}\n```\n\n`+initialize` 的实现\n\n```objective-c\nvoid _class_initialize(Class cls)\n{\n    assert(!cls->isMetaClass());\n\n    Class supercls;\n    bool reallyInitialize = NO;\n\n    \u002F\u002F 确保父类已经初始化完毕，再开始初始化当前类。\n    \u002F\u002F 参见上面关于死锁的说明。\n    supercls = cls->superclass;\n    if (supercls  &&  !supercls->isInitialized()) {\n        _class_initialize(supercls);\n    }\n    \n    \u002F\u002F 尝试以原子方式设置 CLS_INITIALIZING 标志。\n    {\n        monitor_locker_t lock(classInitLock);\n        if (!cls->isInitialized() && !cls->isInitializing()) {\n            cls->setInitializing();\n            reallyInitialize = YES;\n        }\n    }\n    \n    if (reallyInitialize) {\n        \u002F\u002F 我们成功设置了 CLS_INITIALIZING 标志，现在可以初始化该类。\n        \n        \u002F\u002F 记录我们正在初始化这个类，以便后续可以向它发送消息。\n        _setThisThreadIsInitializingClass(cls);\n\n        if (MultithreadedForkChild) {\n            \u002F\u002F 开玩笑啦，我们在 fork() 之后并不会真的调用 +initialize 方法。\n            performForkChildInitialize(cls, supercls);\n            return;\n        }\n        \n        \u002F\u002F 发送 +initialize 消息。\n        \u002F\u002F 注意，如果当前类没有实现 +initialize 方法，则会再次向其父类发送 +initialize 消息。2157218\n        if (PrintInitializing) {\n            _objc_inform(\"INITIALIZE: thread %p: calling +[%s initialize]\",\n                         pthread_self(), cls->nameForLogging());\n        }\n\n        \u002F\u002F 异常处理：如果 +initialize 调用抛出异常，仍然被视为一次完整且成功的初始化。\n        \u002F\u002F\n        \u002F\u002F 只有 __OBJC2__ 添加了这些异常处理机制。!__OBJC2__ 存在与 CF 的 objc_exception_set_functions() 函数之间的启动问题。\n#if __OBJC2__\n        @try\n#endif\n        {\n            callInitialize(cls);\n\n            if (PrintInitializing) {\n                _objc_inform(\"INITIALIZE: thread %p: finished +[%s initialize]\",\n                             pthread_self(), cls->nameForLogging());\n            }\n        }\n#if __OBJC2__\n        @catch (...) {\n            if (PrintInitializing) {\n                _objc_inform(\"INITIALIZE: thread %p: +[%s initialize] \"\n                             \"threw an exception\",\n                             pthread_self(), cls->nameForLogging());\n            }\n            @throw;\n        }\n        @finally\n#endif\n        {\n            \u002F\u002F 初始化完成。\n            lockAndFinishInitializing(cls, supercls);\n        }\n        return;\n    }\n    \n    else if (cls->isInitializing()) {\n        \u002F\u002F 我们无法设置 INITIALIZING 标志，因为该标志已经被其他线程设置。\n        \u002F\u002F 如果是本线程之前设置的，则正常继续。\n        \u002F\u002F 如果是其他线程设置的，则阻塞等待初始化完成。\n        \u002F\u002F 即使在此期间 INITIALIZING 标志变为 INITIALIZED，也没关系，\n        \u002F\u002F   因为我们会在加锁后安全地检查是否已完成初始化。\n        if (_thisThreadIsInitializingClass(cls)) {\n            return;\n        } else if (!MultithreadedForkChild) {\n            waitForInitializeToComplete(cls);\n            return;\n        } else {\n            \u002F\u002F 我们处于 fork() 的子进程端，面对的是在 fork() 时由其他线程正在初始化的类。\n            _setThisThreadIsInitializingClass(cls);\n            performForkChildInitialize(cls, supercls);\n        }\n    }\n    \n    else if (cls->isInitialized()) {\n        \u002F\u002F 设置 CLS_INITIALIZING 失败，因为已经有其他线程完成了初始化。\n        \u002F\u002F 正常继续即可。\n        \u002F\u002F 注意，这一检查必须放在 ISINITIALIZING 检查之后。\n        \u002F\u002F 否则：另一个线程正在初始化该类。ISINITIALIZED 为 false。跳过此分支。然后该线程完成初始化并设置 INITIALIZING=no 和 INITIALIZED=yes。跳过 ISINITIALIZING 分支。结果将非常糟糕。\n        return;\n    }\n    \n    else {\n        \u002F\u002F 不应该走到这一步。\n        _objc_fatal(\"thread-safe class init in objc runtime is buggy!\");\n    }\n}\n\nvoid callInitialize(Class cls)\n{\n    ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize);\n    asm(\"\");\n}\n```\n\n注意看上面的调用了 `callInitialize(cls)` 然后又调用了 `lockAndFinishInitializing(cls, supercls)`。\n\n> 摘自 [iOS App 冷启动治理](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c0a17d6e51d4570cf60d102?utm_source=gold_browser_extension) 一文中对 Dyld 在各阶段所做的事情：\n\n| 阶段         | 工作                                                         |\n| ------------ | ------------------------------------------------------------ |\n| 加载动态库   | Dyld 从主执行文件的 header 获取到需要加载的所依赖动态库列表，然后它需要找到每个 dylib，而应用所依赖的 dylib 文件可能会再依赖其他 dylib，所以所需要加载的是动态库列表一个递归依赖的集合 |\n| Rebase 和 Bind | - Rebase 在 Image 内部调整指针的指向。在过去，会把动态库加载到指定地址，所有指针和数据对于代码都是对的，而现在地址空间布局是随机化，所以需要在原来的地址根据随机的偏移量做一下修正 - Bind 是把指针正确地指向 Image 外部的内容。这些指向外部的指针被符号（symbol）名称绑定，dyld 必须去符号表里查找，找到 symbol 对应的实现 |\n| Objc setup   | - 注册 Objc 类（class registration） - 把 category 的定义插入方法列表（category registration） - 保证每一个 selector 唯一（selector uniqing） |\n| Initializers | - Objc 的 +load() 函数 - C++ 的构造函数属性函数 - 非基本类型的 C++ 静态全局变量的创建（通常是类或结构体） |\n\n最后 dyld 会调用 main() 函数，main() 会调用 UIApplicationMain()，before main() 的过程也就此完成。\n\n### 11. 说说消息转发机制的优劣\n\n## 内存管理\n\n### 1. `weak`的实现原理？`SideTable`的结构是什么样的\n\n> 解答参考自瓜神的[ weak 弱引用的实现方式](https:\u002F\u002Fwww.desgard.com\u002FiOS-Source-Probe\u002FObjective-C\u002FRuntime\u002Fweak%20弱引用的实现方式.html) 。\n\n```objective-c\nNSObject *p = [[NSObject alloc] init];\n__weak NSObject *p1 = p;\n\u002F\u002F ====> 底层是runtime的 objc_initWeak\n\u002F\u002F xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.2 main.m 得不到下面的代码，还是说命令参数不对。\nNSObject objc_initWeak(&p, 对象指针);\n```\n\n通过 runtime 源码可以看到 `objc_initWeak` 实现：\n\n```objective-c\nid\nobjc_initWeakOrNil(id *location, id newObj)\n{\n    if (!newObj) {\n        *location = nil;\n        return nil;\n    }\n\n    return storeWeak\u003CDontHaveOld, DoHaveNew, DontCrashIfDeallocating>\n        (location, (objc_object*)newObj);\n}\n```\nSideTable 结构体在 runtime 底层用于引用计数和弱引用关联表，其数据结构是这样：\n\n```c++\nstruct SideTable {\n    \u002F\u002F 自旋锁\n    spinlock_t slock;\n    \u002F\u002F 引用计数\n    RefcountMap refcnts;\n    \u002F\u002F weak 引用\n    weak_table_t weak_table;\n}\n\nstruct weak_table_t {\n    \u002F\u002F 保存了所有指向指定对象的 weak 指针\n    weak_entry_t *weak_entries;\n    \u002F\u002F 存储空间\n    size_t    num_entries;\n    \u002F\u002F 参与判断引用计数辅助量\n    uintptr_t    mask;\n    \u002F\u002F hash key 最大偏移值\n    uintptr_t    max_hash_displacement;\n};\n```\n\n根据对象的地址在缓存中取出对应的 `SideTable` 实例，\n\n> key 就是对象指针进行哈希算法后的值，全局的 SideTables 表个数根据平台分配了 8 或 64个，因此必定存在多个对象共用同一个 SideTable 的情况，不过SideTable中的 `weak_table_t` 和 `RefcountMap` 又是个哈希表，此时的 key 是对指针进行取反操作，另外还做了哈希碰撞处理。\n\n```c++\n\u002F\u002F objc-private.h\n#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR\n    enum { StripeCount = 8 };\n#else\n    enum { StripeCount = 64 };\n#endif\n\nstatic unsigned int indexForPointer(const void *p) {\n  uintptr_t addr = reinterpret_cast\u003Cuintptr_t>(p);\n  return ((addr >> 4) ^ (addr >> 9)) % StripeCount;\n}\n```\n\n```c++\nstatic SideTable *tableForPointer(const void *p)\n```\n\n或者如上面源码中 ` &SideTables()[newObj]` 方式取表，**这里的 newObj 是实例对象用其指针作为 key 拿到 从全局的 SideTables 中拿到实例自身对应的那张 SideTable**。\n\n```\nstatic StripedMap\u003CSideTable>& SideTables() {\n    return *reinterpret_cast\u003CStripedMap\u003CSideTable>*>(SideTableBuf);\n}\n```\n\n取出实例方法的实现中，使用了 C++ 标准转换运算符 **reinterpret_cast** ，其表达方式为：\n\n```c++\nreinterpret_cast \u003Cnew_type> (expression)\n```\n\n指向某个对象A的所有  weak 关键字修饰的引用都 append 到 `weak_entry_t` 结构体中的`referrers`， 同时`weak_entry_t` 的 `referent` 就是对象A，之后在dealloc 释放时遍历 `weak_table` 遍历时会判断 referent 是否为对象 A 取到 `weak_entry_t`，加入到该 SideTable 中 `weak_table` 中：\n\n> weak 底层实现其实不难，不同架构下 SideTable 表数量也是不同的（8和64）,所以用对象指针作为Key，存在键值冲突的问题，因此在设计上也要解决该问题，源码有体现。\n\n```c++\ntypedef objc_object ** weak_referrer_t;\n\nstruct weak_entry_t {\n    DisguisedPtr\u003Cobjc_object> referent;\n    union {\n        struct {\n            weak_referrer_t *referrers;\n            uintptr_t        out_of_line : 1;\n            uintptr_t        num_refs : PTR_MINUS_1;\n            uintptr_t        mask;\n            uintptr_t        max_hash_displacement;\n        };\n        struct {\n            \u002F\u002F out_of_line=0 is LSB of one of these (don't care which)\n            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];\n        };\n }\n```\n\n旧对象解除注册操作 `weak_unregister_no_lock` 和 新对象添加注册操作 `weak_register_no_lock` ，具体实现可前往 runtime 源码中查看或查看瓜的博文。\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_06eb45bb366b.png)\n\n`weak` 关键字修饰的对象有两种情况：栈上和堆上。上图主要解释 `id referent_id 和 id *referrer_id`，\n\n* 如果是栈上， `referrer` 值为 0x77889900，`referent` 值为 0x11223344\n* 如果是堆上 ， `referrer`  值为 0x1100000+ offset（也就是 weak a 所在堆上的地址），`referent` 值为 0x11223344。\n\n> 如此现在类 A 的实例对象有两个 weak 变量指向它，一个在堆上，一个在栈上。\n\n```c++\nvoid\nweak_unregister_no_lock(weak_table_t *weak_table, id referent_id, \n                        id *referrer_id)\n{\n    objc_object *referent = (objc_object *)referent_id;   \u002F\u002F  0x11223344\n    objc_object **referrer = (objc_object **)referrer_id; \u002F\u002F  0x77889900\n\n    weak_entry_t *entry;\n\n    if (!referent) return;\n\t\t\n  \t\u002F\u002F 从 weak_table 中找到 referent 也就是上面类A的实例对象\n    if ((entry = weak_entry_for_referent(weak_table, referent))) {\n      \t\u002F\u002F 在 entry 结构体中的 referrers 数组中找到指针 referrer 所在位置\n      \t\u002F\u002F 将原本存储 referrer 值的位置置为 nil，相当于做了一个解绑操作\n      \t\u002F\u002F 因为 referrer 要和其他对象建立关系了\n        remove_referrer(entry, referrer);\n        bool empty = true;\n        if (entry->out_of_line()  &&  entry->num_refs != 0) {\n            empty = false;\n        }\n        else {\n            for (size_t i = 0; i \u003C WEAK_INLINE_COUNT; i++) {\n                if (entry->inline_referrers[i]) {\n                    empty = false; \n                    break;\n                }\n            }\n        }\n\n        if (empty) {\n            weak_entry_remove(weak_table, entry);\n        }\n    }\n\n    \u002F\u002F Do not set *referrer = nil. objc_storeWeak() requires that the \n    \u002F\u002F value not change.\n}\n```\n\n> weak 关键字修饰的属性或者变量为什么在对应类实例dealloc后会置为nil，那是因为在类实例释放的时候，dealloc 会从全局的引用计数和weak计数表sideTable**s**中，通过实例地址去找到属于自己的那张表，表中的 weak_table->weak_entries 存储了所有 entry 对象——其实就是所有指向这个实例对象的变量，`weak_entry_t` 中的 `referrers` 数阵存储的就是变量或属性的内存地址，逐一置为nil即可。\n\n\n\n### 2. 关联对象的应用？系统如何实现关联对象的\n\n关联对象基本使用方法：\n\n```objective-c\n#import \u003Cobjc\u002Fruntime.h>\n\nstatic NSString * const kKeyOfImageProperty;\n\n@implementation UIView (Image)\n\n- (UIImage *)pt_image {\n    return objc_getAssociatedObject(self, &kKeyOfImageProperty);\n}\n\n- (void)setPTImage:(UIImage *)image {\n    objc_setAssociatedObject(self, &kKeyOfImageProperty, image,OBJC_ASSOCIATION_RETAIN);\n}\n@end\n```\n\n`objc_AssociationPolicy` 关联对象持有策略有如下几种 ：\n\n| Behavior                            | @property Equivalent                                | Description                                    |\n| ----------------------------------- | --------------------------------------------------- | ---------------------------------------------- |\n| OBJC_ASSOCIATION_ASSIGN             | @property (assign) 或 @property (unsafe_unretained) | 指定一个关联对象的弱引用。                     |\n| OBJC_ASSOCIATION_RETAIN_NONATOMIC   | @property (nonatomic, strong)                       | 指定一个关联对象的强引用，不能被原子化使用。   |\n| OBJC_ASSOCIATION_COPY_NONATOMIC     | @property (nonatomic, copy)                         | 指定一个关联对象的copy引用，不能被原子化使用。 |\n| OBJC_ASSOCIATION_RETAIN             | @property (atomic, strong)                          | 指定一个关联对象的强引用，能被原子化使用。     |\n| OBJC_ASSOCIATION_COPY               | @property (atomic, copy)                            | 指定一个关联对象的copy引用，能被原子化使用。   |\n| OBJC_ASSOCIATION_GETTER_AUTORELEASE |                                                     | 自动释放类型                                   |\n\n> 摘自[瓜地](https:\u002F\u002Fwww.desgard.com\u002FiOS-Source-Probe\u002FObjective-C\u002FRuntime\u002F浅谈Associated%20Objects.html)：OBJC_ASSOCIATION_ASSIGN类型的关联对象和`weak`有一定差别，而更加接近于`unsafe_unretained`，即当目标对象遭到摧毁时，属性值不会自动清空。（翻译自[Associated Objects](http:\u002F\u002Fnshipster.com\u002Fassociated-objects\u002F)）\n>\n> 同样是[Associated Objects](http:\u002F\u002Fnshipster.com\u002Fassociated-objects\u002F)文中，总结了三个关于Associated Objects用法：\n\n> - **为Class添加私有成员**：例如在AFNetworking中，[在UIImageView里添加了**imageRequestOperation**对象](https:\u002F\u002Fgithub.com\u002FAFNetworking\u002FAFNetworking\u002Fblob\u002F2.1.0\u002FUIKit%2BAFNetworking\u002FUIImageView%2BAFNetworking.m#L57-L63)，从而保证了异步加载图片。\n> - **为Class添加共有成员**：例如在FDTemplateLayoutCell中，使用Associated Objects来缓存每个cell的高度（[代码片段1](https:\u002F\u002Fgithub.com\u002Fmconintet\u002FUITableView-FDTemplateLayoutCell\u002Fblob\u002Fmaster\u002FClasses\u002FUITableView+FDIndexPathHeightCache.m#L124)、[代码片段2](https:\u002F\u002Fgithub.com\u002Fmconintet\u002FUITableView-FDTemplateLayoutCell\u002Fblob\u002Fmaster\u002FClasses\u002FUITableView+FDKeyedHeightCache.m#L81)）。通过分配不同的key，在复用cell的时候即时取出，增加效率。\n> - **创建KVO对象**：建议使用category来创建关联对象作为观察者。可以参考[*Objective-C Associated Objects*](http:\u002F\u002Fkingscocoa.com\u002Ftutorials\u002Fassociated-objects\u002F)这篇文的例子。\n\n源码实现非常简单，我添加了完整注释，对c++语法也做了一定解释：\n\n```c++\nid _object_get_associative_reference(id object, void *key) {\n    id value = nil;\n    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;\n    {\n        AssociationsManager manager;\n        \u002F\u002F manager.associations() 返回的是一个 `AssociationsHashMap` 对象(*_map)\n        \u002F\u002F 所以这里 `&associations` 中用了 `&`\n        AssociationsHashMap &associations(manager.associations());\n        \u002F\u002F intptr_t 是为了兼容平台，在64位的机器上，intptr_t和uintptr_t分别是long int、unsigned long int的别名；在32位的机器上，intptr_t和uintptr_t分别是int、unsigned int的别名\n        \u002F\u002F DISGUISE 内部对指针做了 ~ 取反操作，“伪装”？\n        disguised_ptr_t disguised_object = DISGUISE(object);\n        \u002F*\n         AssociationsHashMap 继承自 unordered_map，存储 key-value 的组合\n         iterator find ( const key_type& key )，如果 key 存在，则返回key对象的迭代器，\n         如果key不存在，则find返回 unordered_map::end；因此可以通过 `map.find(key) == map.end()`\n         判断 key 是否存在于当前 map 中。\n         *\u002F\n        AssociationsHashMap::iterator i = associations.find(disguised_object);\n        if (i != associations.end()) {\n            \u002F*\n                unordered_map 的键值分别是迭代器的first和second属性。\n                所以说上面先通过 object 对象(实例对象or类对象) 找到其所有关联对象\n                i->second 取到又是一个 ObjectAssociationMap\n                此刻再通过我们自己设定的 key 来查找对应的关联属性值，不过使用\n                `ObjcAssociation` 封装的\n             *\u002F\n            ObjectAssociationMap *refs = i->second;\n            ObjectAssociationMap::iterator j = refs->find(key);\n            if (j != refs->end()) {\n                ObjcAssociation &entry = j->second;\n                value = entry.value();\n                policy = entry.policy();\n                \u002F\u002F 如果策略是 getter retain ，注意这里留个坑\n                \u002F\u002F 平常 OBJC_ASSOCIATION_RETAIN = 01401\n                \u002F\u002F OBJC_ASSOCIATION_GETTER_RETAIN = (1 \u003C\u003C 8)\n                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {\n                    \u002F\u002F TODO: 有学问\n                    objc_retain(value);\n                }\n            }\n        }\n    }\n    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {\n        objc_autorelease(value);\n    }\n    return value;\n}\n```\n\n对应的set操作实现同样简单，耐心看下源码注释，即使不懂 c++ 都没问题：\n\n```c++\nvoid _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {\n    \u002F\u002F retain the new value (if any) outside the lock.\n    ObjcAssociation old_association(0, nil);\n    \u002F\u002F 如果value对象存在，则进行retain or copy 操作\n    id new_value = value ? acquireValue(value, policy) : nil;\n    {\n        AssociationsManager manager;\n        \u002F\u002F manager.associations() 返回的是一个 `AssociationsHashMap` 对象(*_map)\n        \u002F\u002F 所以这里 `&associations` 中用了 `&`\n        AssociationsHashMap &associations(manager.associations());\n        \u002F\u002F intptr_t 是为了兼容平台，在64位的机器上，intptr_t和uintptr_t分别是long int、unsigned long int的别名；在32位的机器上，intptr_t和uintptr_t分别是int、unsigned int的别名\n        \u002F\u002F DISGUISE 内部对指针做了 ~ 取反操作，“伪装”\n        disguised_ptr_t disguised_object = DISGUISE(object);\n        if (new_value) {\n            \u002F\u002F break any existing association.\n            \u002F*\n             AssociationsHashMap 继承自 unordered_map，存储 key-value 的组合\n             iterator find ( const key_type& key )，如果 key 存在，则返回key对象的迭代器，\n             如果key不存在，则find返回 unordered_map::end；因此可以通过 `map.find(key) == map.end()`\n             判断 key 是否存在于当前 map 中。\n             *\u002F\n            AssociationsHashMap::iterator i = associations.find(disguised_object);\n            \u002F\u002F 这里和get操作不同，set操作时如果查询到对象没有关联对象，那么这一次设值是第一次，\n            \u002F\u002F 所以会创建一个新的 ObjectAssociationMap 用来存储实例对象的所有关联属性\n            if (i != associations.end()) {\n                \u002F\u002F secondary table exists\n                \u002F*\n                    unordered_map 的键值分别是迭代器的first和second属性。\n                    所以说上面先通过 object 对象(实例对象or类对象) 找到其所有关联对象\n                    i->second 受到又是一个 ObjectAssociationMap\n                    此刻再通过我们自己设定的 key 来查找对应的关联属性值，不过使用\n                    `ObjcAssociation` 封装的\n                 *\u002F\n                ObjectAssociationMap *refs = i->second;\n                ObjectAssociationMap::iterator j = refs->find(key);\n                \u002F\u002F 关联属性用 ObjcAssociation 结构体封装\n                if (j != refs->end()) {\n                    old_association = j->second;\n                    j->second = ObjcAssociation(policy, new_value);\n                } else {\n                    (*refs)[key] = ObjcAssociation(policy, new_value);\n                }\n            } else {\n                \u002F\u002F create the new association (first time).\n                ObjectAssociationMap *refs = new ObjectAssociationMap;\n                associations[disguised_object] = refs;\n                (*refs)[key] = ObjcAssociation(policy, new_value);\n                \u002F\u002F 知识点是：newisa.has_assoc = true;\n                object->setHasAssociatedObjects();\n            }\n        } else {\n            \u002F\u002F setting the association to nil breaks the association.\n            AssociationsHashMap::iterator i = associations.find(disguised_object);\n            if (i !=  associations.end()) {\n                ObjectAssociationMap *refs = i->second;\n                ObjectAssociationMap::iterator j = refs->find(key);\n                if (j != refs->end()) {\n                    old_association = j->second;\n                    refs->erase(j);\n                }\n            }\n        }\n    }\n    \u002F\u002F release the old value (outside of the lock).\n    if (old_association.hasValue()) ReleaseValue()(old_association);\n}\n```\n\n\n\n### 3. 关联对象的如何进行内存管理的？关联对象如何实现weak属性\n\n使用了 `policy` 设置内存管理策略，具体见上。凡是加一层呗。。。搞一个类，然后内部封装一个 `weak` 变量持有；或者不用 weak，但是还是封装一层，但是在 dealloc 中进行置为 nil操作\n\n### 4. `Autoreleasepool`的原理？所使用的的数据结构是什么\n\n基于栈为节点（node）的双向链表，使用 `@autoreleasePool` 包裹的作用域中，所有调用 autorelease 都会将对象push到自动释放池，作用域结束就会drain一次，这里涉及到了 哨兵对象，也就是 插入一个nil标识。\n\n### 5. `ARC`的实现原理？`ARC`下对`retain & release`做了哪些优化\n\nARC 是 LLVM 和 Runtime 协作的结果，ARC 中禁止调用 retain\u002Frelease\u002FretainCount\u002Fdealloc方法，新增weak strong。MRC 是手动管理内存。\n\n简单地说，就是代码中自动加入了retain\u002Frelease，原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。ARC并不是GC，它只是一种代码静态分析（Static Analyzer）工具.比如如果不是 alloc\u002Fnew\u002Fcopy\u002FmutableCopy 开头的函数，编译器会将生成的对象自动放入 autoreleasePool 中。如果是 __strong 修饰的变量，编译器会自动给其加上所有权。等等，详细，我们根据不同的关键字来看看编译器为我们具体做了什么。并从中总结出 ARC 的使用规则。\n\n### 6. `ARC`下哪些情况会造成内存泄漏\n\n* 循环引用;\n* **CF类型内存**\u002FC语言malloc出来的对象；\n* **单例也会造成内存泄漏**\n\n## 其他\n\n1. `Method Swizzle`注意事项\n\n2. 属性修饰符`atomic`的内部实现是怎么样的?能保证线程安全吗\n\n   自旋锁，但实际上是unfair 锁，不能完全保证，因为对于容器变量的修改不能保证线程安全，比如 NSMutableArray。\n\n3. iOS 中内省的几个方法有哪些？内部实现原理是什么\n\n4. `class、objc_getClass、object_getclass` 方法有什么区别?\n\n   ```objective-c\n   \u002F 返回的 isa 指针指向的对象，如果 obj 是实例，返回的是类对象，如果 obj 是类对象，返回的是元类对象\n   \u002F\u002F 如果是元类对象，返回的是 NSObject 元类对象（所有元类对象的isa指针都指向了 NSObject 元类对象）\n   Class object_getClass(id obj)\n   {\n       if (obj) return obj->getIsa();\n       else return Nil;\n   }\n   \n   \u002F\u002F 通过传入类名称 返回类对象\n   Class objc_getClass(const char *aClassName)\n   {\n       if (!aClassName) return Nil;\n   \n       \u002F\u002F NO unconnected, YES class handler\n       return look_up_class(aClassName, NO, YES);\n   }\n   ```\n\n   \n\n# 二、NSNotification相关\n\n认真研读、你可以在这里找到答案[轻松过面：一文全解iOS通知机制(经典收藏)](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5e5fc16df265da575155723b)\n\n1. 实现原理（结构设计、通知如何存储的、`name&observer&SEL`之间的关系等）\n2. 通知的发送时同步的，还是异步的\n3. `NSNotificationCenter`接受消息和发送消息是在一个线程里吗？如何异步发送消息\n4. `NSNotificationQueue`是异步还是同步发送？在哪个线程响应\n5. `NSNotificationQueue`和`runloop`的关系\n6. 如何保证通知接收的线程在主线程\n7. 页面销毁时不移除通知会崩溃吗\n8. 多次添加同一个通知会是什么结果？多次移除通知呢\n9. 下面的方式能接收到通知吗？为什么\n\n```\n\u002F\u002F 发送通知\n[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@\"TestNotification\" object:@1];\n\u002F\u002F 接收通知\n[NSNotificationCenter.defaultCenter postNotificationName:@\"TestNotification\" object:nil];\n复制代码\n```\n\n# 三、Runloop & KVO\n\n## runloop\n\n`runloop`对于一个标准的iOS开发来说都不陌生，应该说熟悉`runloop`是标配，下面就随便列几个典型问题吧\n\n### app如何接收到触摸事件的\n\n> [iOS Touch Event from the inside out](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F70ba981317b6)\n\n#### 1 Touch Event 的生命周期\n\n##### 1.1 物理层面事件的生成\n\niPhone 采用电容触摸传感器，利用人体的电流感应工作，由一块四层复合玻璃屏的内表面和夹层各涂有一层导电层，最外层是一层矽土玻璃保护层。当我们手指触摸感应屏的时候，人体的电场让手指和触摸屏之间形成一个耦合电容，对高频电流来说电容是直接导体。于是手指从接触点吸走一个很小的电流，这个电流分从触摸屏的四脚上的电极流出，并且流经这四个电极的电流和手指到四个电极的距离成正比。控制器通过对这四个电流的比例做精确的计算，得出触摸点的距离。\n\n> 更多文献：\n> * [OLED发光原理、面板结构及OLED关键技术深度解析](https:\u002F\u002Fwww.hangjianet.com\u002Fv5\u002FtopicDetail?id=15422809007930000)\n> * [光学触摸屏原理](http:\u002F\u002Fwww.51touch.com\u002Ftechnology\u002Fprinciple\u002F201308\u002F29-24678.html)\n> * [电容触摸屏原理](http:\u002F\u002Fwww.51touch.com\u002Ftechnology\u002Fprinciple\u002F201308\u002F14-24323.html)\n> * [电阻式触摸屏原理(FLASH演示版)](http:\u002F\u002Fwww.51touch.com\u002Ftechnology\u002Fprinciple\u002F201309\u002F04-24800.html)\n> * [iPhone这十年在传感器上的演进](https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F22677100)\n\n##### 1.2 iOS 操作系统下封装和分发事件\n\niOS 操作系统看做是一个处理复杂逻辑的程序，不同进程之间彼此通信采用消息发送方式，即 IPC (Inter-Process Communication)。现在继续说上面电容触摸传感器产生的 Touch Event，它将交由 IOKit.framework 处理封装成 IOHIDEvent 对象；下一步很自然想到通过消息发送方式将事件传递出去，至于发送给谁，何时发送等一系列的判断逻辑又该交由谁处理呢？\n\n答案是 **SpringBoard.app**，它接收到封装好的 **IOHIDEvent** 对象，经过逻辑判断后做进一步的调度分发。例如，它会判断前台是否运行有应用程序，有则将封装好的事件采用 mach port 机制传递给该应用的主线程。\n\nPort 机制在 IPC 中的应用是 Mach 与其他传统内核的区别之一，在 Mach 中，用户进程调用内核交由 IPC 系统。与直接系统调用不同，用户进程首先向内核申请一个 port 的访问许可；然后利用 IPC 机制向这个 port 发送消息，本质还是系统调用，而处理是交由其他进程完成的。\n\n##### 1.3 IOHIDEvent -> UIEvent\n\n应用程序主线程的 runloop 申请了一个 mach port 用于监听 `IOHIDEvent` 的 `Source1` 事件，回调方法是 `__IOHIDEventSystemClientQueueCallback()`，内部又进一步分发 `Source0` 事件，而 `Source0`事件都是自定义的，非基于端口 port，包括触摸，滚动，selector选择器事件，它的回调方法是 `__UIApplicationHandleEventQueue()`，将接收到的 `IOHIDEvent` 事件对象封装成我们熟悉的 `UIEvent` 事件；然后调用 `UIApplication` 实例对象的 `sendEvent:` 方法，将 `UIEvent` 传递给 `UIWindow` 做一些逻辑判断工作：比如触摸事件产生于哪些视图上，有可能有多个，那又要确定哪个是最佳选项呢？ 等等一系列操作。这里先按下不表。\n\n##### 1.4 Hit-Testing 寻找最佳响应者\n\n`Source0` 回调中将封装好的触摸事件 UIEvent（里面有多个UITouch 即手势点击对象），传递给视图 `UIWindow`，其目的在于找到最佳响应者，这个过程称之为 `Hit-Testing`，字面上理解：hit 即触碰了屏幕某块区域，这个区域可能有多个视图叠加而成，那么这个触摸讲道理响应者有多个喽，那么“最佳”又该如何评判？这里要牢记几个规则：\n\n1. 事件是自下而上传递，即 `UIApplication -> UIWindow -> 子视图 -> ...->子视图中的子视图`;\n2. 后加的视图响应程度更高，即更靠近我们的视图;\n3. 如果某个视图不想响应，则传递给比它响应程度稍低一级的视图，若能响应，你还得继续往下传递，若某个视图能响应了，但是没有子视图 它就是最佳响应者。\n4. 寻找最佳响应者的过程中， UIEvent 中的 UITouch 会不断打上标签：比如 `HitTest View` 是哪个，`superview` 是哪个？关联了什么 `Gesture Recognizer`?\n\n那么如何判定视图为响应者？由于 OC 中的类都继承自 `NSObject` ，因此默认判断逻辑已经在`hitTest:withEvent`方法中实现，它有两个作用： 1.询问当前视图是否能够响应事件 2.事件传递的桥梁。若当前视图无法响应事件，返回 nil 。代码如下：\n\n```swift\n- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ \n  \u002F\u002F 1. 前置条件要满足       \n  if (self.userInteractionEnabled == NO || \n  self.hidden == YES ||  \n  self.alpha \u003C= 0.01) return nil;\n  \n  \u002F\u002F 2. 判断点是否在视图内部 这是最起码的 note point 是在当前视图坐标系的点位置\n    if ([self pointInside:point withEvent:event] == NO) return nil;\n\n  \u002F\u002F 3. 现在起码能确定当前视图能够是响应者 接下去询问子视图\n    int count = (int)self.subviews.count;\n    for (int i = count - 1; i >= 0; i--)\n    {\n      \u002F\u002F 子视图\n        UIView *childView = self.subviews[i];\n    \n    \u002F\u002F 点需要先转换坐标系        \n        CGPoint childP = [self convertPoint:point toView:childView];  \n        \u002F\u002F 子视图开始询问\n        UIView *fitView = [childView hitTest:childP withEvent:event]; \n        if (fitView)\n        {\n      return fitView;\n    }\n    }\n                         \n    return self;\n}\n```\n\n1. 首先满足几个前置条件，可交互`userInteractionEnabled=YES`；没有隐藏`self.hidden == NO`；非透明 `self.alpha \u003C= 0.01` ———— 注意一旦不满足上述三个条件，当前视图及其子视图都不能作为响应者，Hit-Testing 判定也止步于此\n2. 接着判断触摸点是否在视图内部 ———— 这个是最基本，无可厚非的判定规则\n3. 此时已经能够说当前视图为响应者，但是不是**最佳**还不能下定论，因此需要进一步传递给子视图判定；注意 `pointInside` 也是默认实现的。\n\n##### 1.5 UIResponder Chain 响应链\n\n`Hit-Testing` 过程中我们无法确定当前视图是否为“最佳”响应者，此时自然还不能处理事件。因此处理机制应该是找到所有响应者以及最佳响应者(**自下而上**)，由它们构成了一条响应链；接着将事件沿着响应链**自上而下**传递下去 ——最顶端自然是最佳响应者，事件除了被响应者消耗，还能被手势识别器或是 `target-action` 模式捕获并消耗。有时候，最佳响应者可能对处理 `Event` “毫无兴趣”，它们不会重写 `touchBegan` `touchesMove`..等四个方法；也不会添加任何手势；但如果是 `control(控件)` 比如 UIButton ，那么事件还是会被消耗掉的。\n\n##### 1.6 UITouch 、 UIEvent 、UIResponder\n\nIOHIDEvent 前面说到是在 IOKit.framwork 中生成的然后经过一系列的分别才到达前台应用，然后应用主线程runloop处理source1回调中又进行source0事件分发，这里有个封装UIEvent的过程，那么 UITouch 呢？ 是不是也是那时候呢？换种思路：一个手指一次触摸屏幕 生成一个 UITouch 对象，内部应该开始进行识别了，因为可能是多个 Touch，并且触摸的先后顺序也不同，这样识别出来的 UIEvent 也不同。所以 UIEvent 对象中包含了触发该事件的触摸对象的集合，通过 allTouches 属性获取。\n\n每个响应者都派生自 UIResponder 类，本身具有相应事件的能力，响应者默认实现 `touchesBegin` `touchesMove` `touchesEnded` `touchesCancelled`四个方法。\n\n事件在未截断的情况下沿着响应链传递给最佳响应者，伪代码如下：\n\n```swift\n0 - [AView touchesBegan:withEvent\n1 - [UIWindow _sendTouchesForEvent]\n2 - [UIWindow sendEvent]           \n3 - [UIApplication sendEvent]      \n4 __dispatchPreprocessEventFromEventQueue\n5 __handleEventQueueInternal\n6 _CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION_\n7 _CFRunLOOPDoSource0\n8 _CFRunLOOPDoSources0\n9 _CFRunLoopRun\n10 _CFRunLoopRunSpecific\n11 GSEventRunModal\n12 UIApplication\n13 main\n14 start\n\n\u002F\u002F UIApplication.m\n- (void)sendEvent {\n  [window sendEvent];\n}\n\n\u002F\u002F UIWindow.m\n- (void)sendEvent{\n  [self _sendTouchesForEvent];\n}\n\n- (void)_sendTouchesForEvent{\n  \u002F\u002Ffind AView Because we know hitTest View\n  [AView touchesBegan:withEvent];\n}\n```\n\n### 为什么只有主线程的`runloop`是开启的\n\n> 这个问题很奇葩，要回答的会就是其他线程并没有调用 `NSRunLoop *runloop = [NSRunLoop currentRunLoop]`，其他还能说啥呢？更多源码分析见网上教程。\n\n```c\nCFRunLoopRef CFRunLoopGetCurrent(void) {\n    CHECK_FOR_FORK();\n    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);\n    if (rl) return rl;\n    return _CFRunLoopGet0(pthread_self());\n}\n```\n\n### 为什么只在主线程刷新UI\n\n> 引用掘金文章[《iOS拾遗——为什么必须在主线程操作UI》](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c406d97e51d4552475fe178)。\n\nUIKit并不是一个 **线程安全** 的类，UI操作涉及到渲染访问各种View对象的属性，如果异步操作下会存在读写问题，而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点`UIApplication`是在主线程进行初始化，所有的用户事件都是在主线程上进行传递（如点击、拖动），所以 view 只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 **同时** 更新，在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。\n\n### `PerformSelector`和`runloop`的关系\n\nperform 有几种方式，如 `[self performSelector:@selector(perform) withObject:nil]` 同步执行的，等同于 objc_msgSend 方法执行调用方法。\n\n而`[self performSelector:@selector(perform) withObject:nil afterDelay:0]` 则是会在当前 runloop 中起一个 timer，如果当前线程没有起runloop(也就是上面说的没有调用 `[NSRunLoop currentRunLoop]` 方法的话)，则不会有输出\n\n```objective-c\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\n        NSLog(@\"[3]\");\n    });\n}\n\n- (void)perform {\n    NSLog(@\"[2] 线程：%@\",[NSThread currentThread]);\n}\n```\n\n> This method sets up a timer to perform the `aSelector` message on the current thread’s run loop. The timer is configured to run in the default mode (`NSDefaultRunLoopMode`). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode. \n>\n> If you want the message to be dequeued when the run loop is in a mode other than the default mode, use the [performSelector:withObject:afterDelay:inModes:](apple-reference-documentation:\u002F\u002FhcVXEPtYGA)method instead. If you are not sure whether the current thread is the main thread, you can use the [performSelectorOnMainThread:withObject:waitUntilDone:](apple-reference-documentation:\u002F\u002FhcChQUeJuZ) or [performSelectorOnMainThread:withObject:waitUntilDone:modes:](apple-reference-documentation:\u002F\u002FhcNxrNoLOP) method to guarantee that your selector executes on the main thread. To cancel a queued message, use the [cancelPreviousPerformRequestsWithTarget:](apple-reference-documentation:\u002F\u002FhcVCAfekYt) or [cancelPreviousPerformRequestsWithTarget:selector:object:](apple-reference-documentation:\u002F\u002Fhc3jvcW4n6) method.\n\n修改代码：\n\n```objective-c\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        NSRunLoop *runloop = [NSRunLoop currentRunLoop]; \n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\u002F\u002F 这里打断点，po runloop ，查看执行这条语句后runloop添加了啥\n        [runloop run];\n        NSLog(@\"[3]\");\n    });\n}\n```\n\n输出如下内容，注意 `[3]` 被输出了，所以说 runloop 并没有被起来，至于原因见下。\n\n```objective-c\n2020-04-07 00:16:43.146357+0800 04-06-performSelector-RunLoop[15486:585028] [1] 线程：\u003CNSThread: 0x6000000cdd00>{number = 6, name = (null)}\n2020-04-07 00:16:43.146722+0800 04-06-performSelector-RunLoop[15486:585028] [2] 线程：\u003CNSThread: 0x6000000cdd00>{number = 6, name = (null)}\n2020-04-07 00:16:43.146932+0800 04-06-performSelector-RunLoop[15486:585028] [3]\n```\n\n所以为了保活可以加个定时器，repeat=YES 的那种。(ps: repeat=NO 的那种复习下 timer 的循环引用会自动打破！)\n\n```objective-c\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {\n            NSLog(@\"timer 定时任务\");\n        }];\n        \n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        NSRunLoop *runloop = [NSRunLoop currentRunLoop];\n        [runloop addTimer:timer forMode:NSDefaultRunLoopMode];\n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\n        [runloop run];\n        NSLog(@\"[3]\");\n    });\n}\n```\n\nRunLoop 添加观察者代码：\n\n```objective-c\n\u002F*\n kCFRunLoopEntry = (1UL \u003C\u003C 0),1\n kCFRunLoopBeforeTimers = (1UL \u003C\u003C 1),2\n kCFRunLoopBeforeSources = (1UL \u003C\u003C 2), 4\n kCFRunLoopBeforeWaiting = (1UL \u003C\u003C 5), 32\n kCFRunLoopAfterWaiting = (1UL \u003C\u003C 6), 64\n kCFRunLoopExit = (1UL \u003C\u003C 7),128\n kCFRunLoopAllActivities = 0x0FFFFFFFU\n *\u002F\nstatic void addRunLoopObserver() {\n    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {\n        switch (activity) {\n            case kCFRunLoopEntry: {\n                NSLog(@\"即将进入 runloop\");\n            }\n                break;\n            case kCFRunLoopBeforeTimers: {\n                NSLog(@\"定时器 timers 之前\");\n            }\n                break;\n            case kCFRunLoopBeforeSources: {\n                NSLog(@\"Sources 事件前\");\n            }\n                break;\n            case kCFRunLoopBeforeWaiting: {\n                NSLog(@\"RunLoop 即将进入休眠\");\n            }\n                break;\n            case kCFRunLoopAfterWaiting: {\n                NSLog(@\"RunLoop 唤醒后\");\n            }\n                break;\n            case kCFRunLoopExit: {\n                NSLog(@\"退出\");\n            }\n                break;\n            default:\n                break;\n        }\n    });\n  \t\u002F\u002F 这里 CFRunLoopGetCurrent 创建了当前线程的 runloop 对象\n    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);\n    CFRelease(observer);\n}\n\n- (IBAction)test01:(id)sender {\n    dispatch_async(self.concurrencyQueue2, ^{\n        NSLog(@\"[1] 线程：%@\",[NSThread currentThread]);\n        NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {\n            NSLog(@\"timer 定时任务\");\n        }];\n        addRunLoopObserver();\n        \u002F\u002F 当前线程没有开启 runloop 所以改方法是没办法执行的\n        NSRunLoop *runloop = [NSRunLoop currentRunLoop];\n        [runloop addTimer:timer forMode:NSDefaultRunLoopMode];\n        [self performSelector:@selector(perform) withObject:nil afterDelay:0];\n        [runloop run];\n        NSLog(@\"[3]\");\n    });\n}\n```\n\n>  参考文献：\n>\n>  * [Runloop与performSelector](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c70b391e51d451646267db1)\n\n### 如何使线程保活\n\n> 线程保活就是不让线程退出，所以往简单说就是搞个 “while(1)” 自己实现一套处理流程，事件派发就可以了；但 iOS 中有 runloop，所以我们就无须大费周章。 TODO： C 语言嵌入式如何实现线程池和保活。\n\nrunloop 线程保活前提就是有事情要处理，这里指 timer，source0，source1 事件。\n\n所以有如下几种方式，方式一：\n\n```objective-c\nNSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {\n   NSLog(@\"timer 定时任务\");\n}];\nNSRunLoop *runloop = [NSRunLoop currentRunLoop];\n[runloop addTimer:timer forMode:NSDefaultRunLoopMode];\n[runloop run];\n```\n\n方式二：\n\n```objective-c\nNSRunLoop *runLoop = [NSRunLoop currentRunLoop];\n[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];\n[runLoop run];\n```\n\n方式三：\n\n```objectivec\n- (IBAction)testRunLoopKeepAlive:(id)sender {\n    self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(start) object:nil];\n    [self.myThread start];\n}\n\n- (void)start {\n    self.finished = NO;\n    do {\n      \t\u002F\u002F Runs the loop until the specified date, during which time it processes data from all attached input sources.\n        [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];\n    } while (!self.finished);\n}\n- (IBAction)closeRunloop:(id)sender {\n    self.finished = YES;\n}\n\n- (IBAction)executeTask:(id)sender {\n    [self performSelector:@selector(doTask) onThread:self.myThread withObject:nil waitUntilDone:NO];\n}\n\n- (void)doTask {\n    NSLog(@\"执行任务在线程：%@\",[NSThread currentThread]);\n}\n```\n\n> 如果没有输入源或计时器附加到运行循环，此方法会立即退出并返回 `NO`；否则，它会在第一个输入源被处理或达到 `limitDate` 后返回。手动从运行循环中移除所有已知的输入源和计时器并不能保证运行循环会立即退出。macOS 可能会根据需要安装和移除额外的输入源，以处理针对接收者线程的请求。这些源可能会因此阻止运行循环退出。\n\n所以上面的方式并非完美，只要没有源，runloop 直接就被退出了，但是因为包了一个while (!self.finished)，所以相当于退出->起->退出-> 起。\n\n> 注意：如果runloop中没有处理事件，这里一直会退出然后起runloop，就算设置了 `[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];` 也没用，但是执行过一次 `[self performSelector:@selector(doTask) onThread:self.myThread withObject:nil waitUntilDone:NO]`，那么程序就卡在 `[NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]` 这一行了。\n\n## KVO\n\n同`runloop`一样，这也是标配的知识点了，同样列出几个典型问题\n\n### 1. 实现原理\n\nKVO 会为需要observed的对象动态创建一个子类，以`NSKVONotifying_` 最为前缀，然后将对象的 isa 指针指向新的子类，同时重写 class 方法，返回原先类对象，这样外部就无感知了；其次重写所有要观察属性的setter方法，统一会走一个方法，然后内部是会调用 `willChangeValueForKey` 和 `didChangevlueForKey` 方法，在一个被观察属性发生改变之前， `willChangeValueForKey:`一定会被调用，这就 会记录旧的值。而当改变发生后，`didChangeValueForKey:`会被调用，继而 `observeValueForKey:ofObject:change:context:` 也会被调用。\n\n![图片出处https:\u002F\u002Fjuejin.im\u002Fpost\u002F5adab70cf265da0b736d37a8](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_17f21aeddcec.png)\n\n那么如何验证上面的说法呢？很简单，借助runtime 即可，测试代码请点击[这里](https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record\u002Ftree\u002Fmaster\u002Fsamples\u002F02-25-KVO):\n\n```objective-c\n- (void)viewDidLoad {\n    [super viewDidLoad];\n    self.person = [[Person alloc] initWithName:@\"pmst\" age:18];\n    self.teacher = [[Teacher alloc] initWithName:@\"ppp\" age:28];\n    self.teacher.work = @\"数学\";\n    self.teacher.numberOfStudent = 10;\n    \n    NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;\n    \n    RuntimeUtil *utils = [RuntimeUtil new];\n    [utils logClassInfo:self.person.class];\n    [self.person addObserver:self forKeyPath:@\"age\" options:options context:nil];\n    [utils logClassInfo:object_getClass(self.person)];\n    \n    \n    [utils logClassInfo:self.teacher.class];\n    [self.teacher addObserver:self forKeyPath:@\"age\" options:options context:nil];\n    [self.teacher addObserver:self forKeyPath:@\"name\" options:options context:nil];\n    [self.teacher addObserver:self forKeyPath:@\"work\" options:options context:nil];\n    [utils logClassInfo:object_getClass(self.teacher)];\n}\n```\n\n这里 `object_getClass()` 方法实现也贴一下，如果直接使用 `.class` 那么因为被重写过，返回的还是原先对象的类对象，而直接用 runtime 方法的直接返回了 `isa` 指针。\n\n```objective-c\nClass object_getClass(id obj)\n{\n    if (obj) return obj->getIsa();\n    else return Nil;\n}\n```\n\n通过日志确实可以看到子类重写了对应属性的setter方法：\n\n```shell\n2020-03-25 23:11:00.607820+0800 02-25-KVO[28370:1005147] LOG:(NSKVONotifying_Teacher) INFO\n2020-03-25 23:11:00.608190+0800 02-25-KVO[28370:1005147] ==== OUTPUT:NSKVONotifying_Teacher properties ====\n2020-03-25 23:11:00.608529+0800 02-25-KVO[28370:1005147] ==== OUTPUT:NSKVONotifying_Teacher Method ====\n2020-03-25 23:11:00.608876+0800 02-25-KVO[28370:1005147] method name:setWork:\n2020-03-25 23:11:00.609219+0800 02-25-KVO[28370:1005147] method name:setName:\n2020-03-25 23:11:00.646713+0800 02-25-KVO[28370:1005147] method name:setAge:\n2020-03-25 23:11:00.646858+0800 02-25-KVO[28370:1005147] method name:class\n2020-03-25 23:11:00.646971+0800 02-25-KVO[28370:1005147] method name:dealloc\n2020-03-25 23:11:00.647088+0800 02-25-KVO[28370:1005147] method name:_isKVOA\n2020-03-25 23:11:00.647207+0800 02-25-KVO[28370:1005147] =========================\n```\n\n>  疑惑点：看到有文章提出 KVO 之后，setXXX 方法转而调用 `_NSSetBoolValueAndNotify、_NSSetCharValueAndNotify、_NSSetFloatValueAndNotify、_NSSetLongValueAndNotify` 等方法，但是通过 runtime 打印 method 是存在的，猜测 SEL 是一样的，但是 IMP 被换掉了，关于源码的实现还未找到。TODO下。\n\n### 2. 如何手动关闭kvo\n\n> KVO 和 KVC 相关接口太多，实际开发中直接查看接口文档即可。\n\n```objective-c\n+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{\n    if ([key isEqualToString:@\"name\"]) {\n        return NO;\n    }else{\n        return [super automaticallyNotifiesObserversForKey:key];\n    }\n}\n\n-(void)setName:(NSString *)name{\n    \n    if (_name!=name) {\n        \n        [self willChangeValueForKey:@\"name\"];\n        _name=name;\n        [self didChangeValueForKey:@\"name\"];\n    }\n      \n}\n```\n\n### 3. 通过KVC修改属性会触发KVO么\n\n会触发 KVO 操作，KVC 时候会先查询对应的 getter 和 setter 方法，如果都没找到，调用 \n\n```objective-c\n+ (BOOL)accessInstanceVariablesDirectly {\n    return NO;\n}\n```\n\n如果返回 YES，那么可以直接修改实例变量。\n\n* KVC 调用 getter 流程：`getKEY，KEY，isKEY, _KEY`，接着是实例变量 `_KEY,_isKEY, KEY, isKEY`;\n\n* KVC 调用 setter 流程：`setKEY`和 `_setKEY`，实例变量顺序 `_KEY,_isKEY, KEY, isKEY`，没找到就调用 `setValue: forUndefinedKey:`\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_c19cd4346db4.jpg)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_94e533300d59.jpg)\n\n### 4. 哪些情况下使用KVO会崩溃，怎么防护崩溃\n\n1. dealloc 没有移除 KVO 观察者，解决方案：创建一个中间对象，将其作为某个属性的观察者，然后 dealloc 时候再移除观察者，而调用者是持有中间对象的，调用者释放了，中间对象也释放了，dealloc 也就移除观察者了；\n2. 多次重复移除同一个属性，移除了未注册的观察者\n3. 被观察者提前被释放，被观察者在 dealloc 时仍然注册着 KVO，导致崩溃。 例如：被观察者是局部变量的情况（iOS 10 及之前会崩溃） 比如 weak ；\n4. 添加了观察者，但未实现 `observeValueForKeyPath:ofObject:change:context:`方法，导致崩溃；\n5. 添加或者移除时 `keypath == nil`，导致崩溃；\n\n> 以下解决方案出自 [iOS 开发：『Crash 防护系统』（二）KVO 防护](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5d67b720f265da039a289bb4) 一文。\n\n**解决方案一：**\n\nFBKVOController 对 KVO 机制进行了额外的一层封装，框架不但可以自动帮我们移除观察者，还提供了 block 或者 selector 的方式供我们进行观察处理。不可否认的是，FBKVOController 为我们的开发提供了很大的便利性。但是相对而言，这种方式对项目代码的侵入性比较大，必须依靠编码规范来强制约束团队人员使用这种方式。\n\n**解决方案二：**\n\n1. 首先为 NSObject 建立一个分类，利用 Method Swizzling，实现自定义的 `BMP_addObserver:forKeyPath:options:context:`、`BMP_removeObserver:forKeyPath:`、`BMP_removeObserver:forKeyPath:context:`、`BMPKVO_dealloc`方法，用来替换系统原生的添加移除观察者方法的实现。\n\n2. 然后在观察者和被观察者之间建立一个 `KVODelegate 对象`，两者之间通过 `KVODelegate 对象` 建立联系。然后在添加和移除操作时，将 KVO 的相关信息例如 `observer`、`keyPath`、`options`、`context` 保存为 `KVOInfo 对象`，并添加到 `KVODelegate 对象` 中对应 的 `关系哈希表` 中，对应原有的添加观察者。 关系哈希表的数据结构：`{keypath : [KVOInfo 对象1, KVOInfo 对象2, ... ]}`\n\n3. 在添加和移除操作的时候，利用 `KVODelegate 对象` 做转发，把真正的观察者变为 `KVODelegate 对象`，而当被观察者的特定属性发生了改变，再由 `KVODelegate 对象` 分发到原有的观察者上。\n\n1. **添加观察者时**：通过关系哈希表判断是否重复添加，只添加一次。\n2. **移除观察者时**：通过关系哈希表是否已经进行过移除操作，避免多次移除。\n3. **观察键值改变时**：同样通过关系哈希表判断，将改变操作分发到原有的观察者上。\n\n**解决方案三：**\n\n**XXShield** 实现方案和 BayMax 系统类似。也是利用一个 Proxy 对象用来做转发， 真正的观察者是 Proxy，被观察者出现了通知信息，由 Proxy 做分发。不过不同点是 Proxy 里面保存的内容没有前者多。只保存了 `_observed（被观察者）` 和关系哈希表，这个关系哈希表中只维护了 `keyPath` 和 `observer` 的关系。\n\n关系哈希表的数据结构：`{keypath : [observer1, observer2 , ...](NSHashTable)}` 。\n\nXXShield 在 dealloc 中也做了类似将多余观察者移除掉的操作，是通过关系数据结构和 `_observed` ，然后调用原生移除观察者操作实现的。\n\n### 5. KVO的优缺点\n\n优点：\n\n1. 运用了设计模式：**观察者模式**\n2. 支持**多个观察者观察同一属性**，或者**一个观察者监听不同属性**。\n3. 开发人员不需要实现属性值变化了发送通知的方案，系统已经封装好了，大大减少开发工作量；\n4. 能够对非我们创建的对象，即内部对象的状态改变作出响应，而且不需要改变内部对象（SDK对象）的实现；\n5. 能够提供观察的属性的最新值以及先前值；\n6. 用key paths来观察属性，因此也可以观察嵌套对象；\n7. 完成了对观察对象的抽象，因为不需要额外的代码来允许观察值能够被观察\n\n缺点：\n\n1. 观察的属性键值硬编码（字符串），编译器不会出现警告以及检查；\n2. 由于允许对一个对象进行不同属性观察，所以在唯一回调方法中，会出现地狱式 `if-else if - else` 分支处理情况；\n\n> References：\n>\n> * [iOS底层原理总结篇-- 深入理解 KVC\\KVO 实现机制](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5c2189dee51d454517589c8b)\n> * [iOS 开发：『Crash 防护系统』（二）KVO 防护](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5d67b720f265da039a289bb4)\n> *  [ValiantCat](https:\u002F\u002Fgithub.com\u002FValiantCat) \u002F **[XXShield](https:\u002F\u002Fgithub.com\u002FValiantCat\u002FXXShield)**（第三方框架）\n> *  [JackLee18](https:\u002F\u002Fgithub.com\u002FJackLee18) \u002F **[JKCrashProtect](https:\u002F\u002Fgithub.com\u002FJackLee18\u002FJKCrashProtect)**（第三方框架）\n> * [大白健康系统 -- iOS APP运行时 Crash 自动修复系统](https:\u002F\u002Fneyoufan.github.io\u002F2017\u002F01\u002F13\u002Fios\u002FBayMax_HTSafetyGuard)\n\n# 四、Block\n\n1. `block`的内部实现，结构体是什么样的\n\n   block 也是一个对象，主要分为 Imp 结构体 和 Desc 结构体，用 `clang -rewrite-objc` 命令将 oc 代码重写成 c++:\n\n   ```c++\n   struct __block_impl {\n     void *isa;\n     int Flags;\n     int Reserved;\n     void *FuncPtr;\n   };\n   \n   struct __main_block_impl_0 {\n     struct __block_impl impl;\n     struct __main_block_desc_0* Desc;\n     __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {\n       impl.isa = &_NSConcreteStackBlock;\n       impl.Flags = flags;\n       impl.FuncPtr = fp;\n       Desc = desc;\n     }\n   };\n   ```\n\n   > 重写c++只是帮助我们理解，实际实现还是有偏差的，转成 IR 是不是更好呢？\n\n2. block是类吗，有哪些类型\n\n   一般，block有三种：_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock，根据Block对象创建时所处数据区不同而进行区别。\n\n   1. 栈上 Block，引用了栈上变量，生命周期由系统控制的，一旦所属作用域结束，就被系统销毁了。\n   2. 堆上 Block，使用 copy 或者 strong（ARC）下就从栈Block 拷贝到堆上。\n   3. 全局 Block，未引用任何栈上变量时就是全局Block;\n\n3. 一个`int`变量被 `__block` 修饰与否的区别？block的变量截获\n\n   值 copy 和指针 copy，`__block` 修饰的话允许在 block 内部修改变量，因为传入的是 int变量的指针。\n\n   外部变量有四种类型：自动变量、静态变量、静态全局变量、全局变量。\n\n   全局变量和静态全局变量在 block 中是直接引用的，不需要通过结构去传入指针；\n\n   函数\u002F方法中的 static 静态变量是直接在block中保存了指针，如下测试代码：\n\n   ```c++\n   int a = 1;\n   static int b = 2;\n   \n   int main(int argc, const char * argv[]) {\n   \n       int c = 3;\n       static int d = 4;\n       NSMutableString *str = [[NSMutableString alloc]initWithString:@\"hello\"];\n       void (^blk)(void) = ^{\n           a++;\n           b++;\n           d++;\n           [str appendString:@\"world\"];\n           NSLog(@\"1----------- a = %d,b = %d,c = %d,d = %d,str = %@\",a,b,c,d,str);\n       };\n       \n       a++;\n       b++;\n       c++;\n       d++;\n   str = [[NSMutableString alloc]initWithString:@\"haha\"];\n       NSLog(@\"2----------- a = %d,b = %d,c = %d,d = %d,str = %@\",a,b,c,d,str);\n       blk();\n       \n       return 0;\n   }\n   ```\n\n   转成  c++ 代码：\n\n   ```objective-c\n   struct __block_impl {\n     void *isa;\n     int Flags;\n     int Reserved;\n     void *FuncPtr;\n   };\n   \n   int a = 1; \u002F\u002F \u003C------------------- NOTE\n   static int b = 2; \u002F\u002F \u003C------------------- NOTE\n   struct __main_block_impl_0 {\n     struct __block_impl impl;\n     struct __main_block_desc_0* Desc;\n     int *d;\t\t\t\t\t\t\u002F\u002F \u003C------------------- NOTE\n     NSMutableString *str;\t\t\t\t\u002F\u002F \u003C------------------- NOTE\n     int c; \u002F\u002F \u003C------------------- NOTE\n     __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {\n       impl.isa = &_NSConcreteStackBlock;\n       impl.Flags = flags;\n       impl.FuncPtr = fp;\n       Desc = desc;\n     }\n   };\n   \n   static void __main_block_func_0(struct __main_block_impl_0 *__cself) {\n     int *d = __cself->d; \u002F\u002F bound by copy\n     NSMutableString *str = __cself->str; \u002F\u002F bound by copy\n     int c = __cself->c; \u002F\u002F bound by copy\n   \n           a++;\n           b++;\n           (*d)++;\n           ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName(\"appendString:\"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_1);\n           NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_2,a,b,c,(*d),str);\n       }\n   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\u002F*BLOCK_FIELD_IS_OBJECT*\u002F);}\n   \n   static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3\u002F*BLOCK_FIELD_IS_OBJECT*\u002F);}\n   \n   static struct __main_block_desc_0 {\n     size_t reserved;\n     size_t Block_size;\n     void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);\n     void (*dispose)(struct __main_block_impl_0*);\n   } __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};\n   \n   int main(int argc, const char * argv[]) {\n       int c = 3;\n       static int d = 4;\n       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_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_0);\n       void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));\n   \n       a++;\n       b++;\n       c++;\n       d++;\n       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_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);\n       NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_4,a,b,c,d,str);\n       ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);\n   \n       return 0;\n   }\n   ```\n\n   \n\n4. `block`在修改`NSMutableArray`，需不需要添加`__block`\n\n   不需要，本身 block 内部就捕获了 NSMutableArray 指针，除非你要修改指针指向的对象，而这里明显只是修改内存数据，这个可以类比 NSMutableString。\n\n5. 怎么进行内存管理的\n\n   `static void *_Block_copy_internal(const void *arg, const int flags)` 和 `void _Block_release(void *arg) `\n\n   > 推荐[iOS Block原理探究以及循环引用的问题](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F9ff40ea1cee5) 一文。\n\n6. `block`可以用`strong`修饰吗\n\n   ARC 貌似是可以的， strong 和 copy 的操作都是将栈上block 拷贝到堆上。TODO：确认下。\n\n7. 解决循环引用时为什么要用`__strong、__weak`修饰\n\n   `__weak` 就是为了避免 retainCycle，而block 内部 `__strong` 则是在作用域 retain 持有当前对象做一些操作，结束后会释放掉它。\n\n8. `block`发生`copy`时机\n\n   block 从栈上拷贝到堆上几种情况：\n\n   * 调用Block的copy方法\n\n   * 将Block作为函数返回值时\n\n   * 将Block赋值给__strong修饰的变量或Block类型成员变量时\n\n   * 向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时\n\n9. `Block`访问对象类型的`auto变量`时，在`ARC和MRC`下有什么区别\n\n# 四、Block 原理探究代码篇\n\n首先明确 Block 底层数据结构，之后所有的 demos 都基于此来学习知识点：\n\n```c\ntypedef NS_OPTIONS(int,PTBlockFlags) {\n    PTBlockFlagsHasCopyDisposeHelpers = (1 \u003C\u003C 25),\n    PTBlockFlagsHasSignature          = (1 \u003C\u003C 30)\n};\ntypedef struct PTBlock {\n    __unused Class isa;\n    PTBlockFlags flags;\n    __unused int reserved;\n    void (__unused *invoke)(struct PTBlock *block, ...);\n    struct {\n        unsigned long int reserved;\n        unsigned long int size;\n        \u002F\u002F requires PTBlockFlagsHasCopyDisposeHelpers\n        void (*copy)(void *dst, const void *src);\n        void (*dispose)(const void *);\n        \u002F\u002F requires PTBlockFlagsHasSignature\n        const char *signature;\n        const char *layout;\n    } *descriptor;\n    \u002F\u002F imported variables\n  \t\u002F\u002F Block 捕获的实例变量都在次\n} *PTBlockRef;\n\ntypedef struct PTBlock_byref {\n    void *isa;\n    struct PTBlock_byref *forwarding;\n    volatile int flags; \u002F\u002F contains ref count\n    unsigned int size;\n    \u002F\u002F 下面两个函数指针是不定的 要根据flags来\n\u002F\u002F    void (*byref_keep)(struct PTBlock_byref *dst, struct PTBlock_byref *src);\n\u002F\u002F    void (*byref_destroy)(struct PTBlock_byref *);\n    \u002F\u002F long shared[0];\n} *PTBlock_byref_Ref;\n```\n\n## 1. 调用 block\n\n```c\nvoid (^blk)(void) = ^{\n  NSLog(@\"hello world\");\n};\nPTBlockRef block = (__bridge PTBlockRef)blk;\nblock->invoke(block);\n```\n\n## 2. block 函数签名\n\n```c\nvoid (^blk)(int, short, NSString *) = ^(int a, short b, NSString *str){\n  NSLog(@\"a:%d b:%d str:%@\",a,b,str);\n};\nPTBlockRef block = (__bridge PTBlockRef)blk;\nif (block->flags & PTBlockFlagsHasSignature) {\n  void *desc = block->descriptor;\n  desc += 2 * sizeof(unsigned long int);\n  if (block->flags & PTBlockFlagsHasCopyDisposeHelpers) {\n    desc += 2 * sizeof(void *);\n  }\n\n  const char *signature = (*(const char **)desc);\n  NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:signature];\n  NSLog(@\"方法 signature:%s\",signature);\n}\n\n\u002F\u002F 打印内容如下:\n\u002F\u002F v24 @?0 i8 s12 @\"NSString\"16\n\u002F\u002F 其中 ? 是 An unknown type (among other things, this code is used for function pointers)\n```\n\n## 3. block 捕获栈上局部变量\n\n捕获的变量都会按照顺序放置在 `PTBlock` 结构体后面，如此看来就是个变长结构体。\n\n也就是说我们可以通过如下方式知道 block 捕获了哪些外部变量（全局变量除外）。\n\n```c\nint a = 0x11223344;\nint b = 0x55667788;\nNSString *str = @\"pmst\";\nvoid (^blk)(void) = ^{\n  NSLog(@\"a:%d b:%d str:%@\",a,b, str);\n};\nPTBlockRef block = (__bridge PTBlockRef)blk;\nvoid *pt = (void *)block + sizeof(struct PTBlock);\nlong long *ppt = pt;\nNSString *str_ref = (__bridge id)((void *)(*ppt));\nint *a_ref = pt + sizeof(NSString *);\nint *b_ref = pt + sizeof(NSString *) + sizeof(int);\n\nNSLog(@\"a:0x%x b:0x%x str:%@\",*a_ref, *b_ref, str_ref);\n```\n\n> TODO：`NSString` layout 布局为何在第一位？\n\n## 4. `__block` 变量（栈上）\n\n```c\n__block int a = 0x99887766;\n__unsafe_unretained void (^blk)(void) = ^{\n  NSLog(@\"__block a :%d\",a);\n};\nNSLog(@\"Block 类型 %@\",[blk class]);\nPTBlockRef block = (__bridge PTBlockRef)blk;\nvoid *pt = (void *)block + sizeof(struct PTBlock);\nlong long *ppt = pt;\nvoid *ref = (PTBlock_byref_Ref)(*ppt);\nvoid *shared = ref + sizeof(struct PTBlock_byref);\nint *a_ref = (int *)shared;\nNSLog(@\"a 指针：%p block a 指针:%p block a value:0x%x\",&a, a_ref,*a_ref);\nNSLog(@\"PTBlock_byref 指针：%p\",ref);\nNSLog(@\"PTBlock_byref forwarding 指针：%p\",((PTBlock_byref_Ref)ref)->forwarding);\n\u002F*\n输出如下：\nBlock 类型 __NSStackBlock__\na 指针：0x7ffeefbff528 block a 指针:0x7ffeefbff528 block a value:0x99887766\nPTBlock_byref 指针：0x7ffeefbff510\nPTBlock_byref forwarding 指针：0x7ffeefbff510\n*\u002F\n```\n\n可以看到 `__block int a` 已经变成了另外一个数据结构了，打印地址符合预期，此刻 block 以及其他的变量结构体都在栈上。\n\n## 5.  `__block` 变量，[block copy] 后的内存变化\n\n```c\n__block int a = 0x99887766;\n__unsafe_unretained void (^blk)(NSString *) = ^(NSString *flag){\n  NSLog(@\"[%@] 中 a 地址:%p\",flag, &a);\n};\nNSLog(@\"blk 类型 %@\",[blk class]);\nblk(@\"origin block\");\nvoid (^copyblk)(NSString *) = [blk copy];\ncopyblk(@\"copy block\");\nblk(@\"origin block 二次调用\");\n\u002F**\n\t输出如下：\nblk 类型 __NSStackBlock__\n[origin block] 中 a 地址:0x7ffeefbff528\ncopyblk 类型 __NSMallocBlock__\n[copy block] 中 a 地址:0x102212468\n[origin block 二次调用] 中 a 地址:0x102212468\n*\u002F\n```\n\n很明显对 blk 进行 copy 操作后，copyblk 已经“移驾”到堆上，随着拷贝的还有 `__block` 修饰的a变量（`PTBlock_byref_Ref `类型）；\n\n## 6. `__block` 变量中 forwarding 指针\n\n```c\n__block int a = 0x99887766;\n__unsafe_unretained void (^blk)(NSString *,id) = ^(NSString *flag, id bblk){\n  NSLog(@\"[%@] a address:%p\",flag, &a); \u002F\u002F a 取值都是 ->forwarding->a 方式\n  PTBlockRef block = (__bridge PTBlockRef)bblk;\n  void *pt = (void *)block + sizeof(struct PTBlock);\n  long long *ppt = pt;\n  void *ref = (PTBlock_byref_Ref)(*ppt);\n  NSLog(@\"[%@] PTBlock_byref_Ref 指针：%p\",flag,ref);\n  NSLog(@\"[%@] PTBlock_byref_Ref forwarding 指针：%p\",flag,((PTBlock_byref_Ref)ref)->forwarding);\n  void *shared = ref + sizeof(struct PTBlock_byref;\n  int *a_ref = (int *)shared;\n  NSLog(@\"[%@] a value : 0x%x a adress:%p\", flag, *a_ref, a_ref);\n\n};\nNSLog(@\"blk 类型 %@\",[blk class]);\nblk(@\"origin block\", blk);\nvoid (^copyblk)(NSString *,id) = [blk copy];\nNSLog(@\"copyblk 类型 %@\",[copyblk class]);\ncopyblk(@\"copy block\",copyblk);\nblk(@\"origin block after copy\", blk);\n\u002F**\nMRC 模式下输出：\nblk 类型 __NSStackBlock__\n[origin block] a address:0x7ffeefbff528\n[origin block] PTBlock_byref_Ref 指针：0x7ffeefbff510\n[origin block] PTBlock_byref_Ref forwarding 指针：0x7ffeefbff510\n[origin block] a value : 0x99887766 a adress:0x7ffeefbff528\ncopyblk 类型 __NSMallocBlock__\n[copy block] a address:0x1032041d8\n[copy block] PTBlock_byref_Ref 指针：0x1032041c0\n[copy block] PTBlock_byref_Ref forwarding 指针：0x1032041c0\n[copy block] a value : 0x99887766 a adress:0x1032041d8\n[origin block after copy] a address:0x1032041d8\n[origin block after copy] PTBlock_byref_Ref 指针：0x7ffeefbff510\n[origin block after copy] PTBlock_byref_Ref forwarding 指针：0x1032041c0\n[origin block after copy] a value : 0x99887766 a adress:0x7ffeefbff528\n\nARC 模式下输出（这个稍有出路）：\nblk 类型 __NSStackBlock__\n[origin block] a address:0x100604cc8\n[origin block] PTBlock_byref_Ref 指针：0x100604cb0\n[origin block] PTBlock_byref_Ref forwarding 指针：0x100604cb0\n[origin block] a value : 0x99887766 a adress:0x100604cc8\ncopyblk 类型 __NSMallocBlock__\n[copy block] a address:0x100604cc8\n[copy block] PTBlock_byref_Ref 指针：0x100604cb0\n[copy block] PTBlock_byref_Ref forwarding 指针：0x100604cb0\n[copy block] a value : 0x99887766 a adress:0x100604cc8\n*\u002F\n```\n\n这里可以看到 forwarding 指针确实指向了结构体本身，随着 copy 行为确实进行了一次栈->堆的赋值——`block`和 `__block` 变量。\n\n> 建议用 lldb 命令去看内存布局。\n\n## 7. Block Hook\n\nTODO:\n\n\n\n# 五、多线程\n\n# \n\n## `iOS`开发中有多少类型的线程？分别对比\n\n* NSThread，每个 NSThread对象对应一个线程，量级较轻，通常我们会起一个 runloop 保活，然后通过添加自定义source0源或者 perform onThread 来进行调用，优点轻量级，使用简单，缺点：需要自己管理线程的生命周期，保活，另外还会线程同步，加锁、睡眠和唤醒。\n* GCD：Grand Central Dispatch（派发） 是基于C语言的框架，可以充分利用多核，是苹果推荐使用的多线程技术\n  * 优点：GCD更接近底层，而NSOperationQueue则更高级抽象，所以GCD在追求性能的底层操作来说，是速度最快的，有待确认\n  * 缺点：操作之间的事务性，顺序行，依赖关系。GCD需要自己写更多的代码来实现\n* NSOperation\n  * 优点： 使用者的关注点都放在了 operation 上，而不需要线程管理。\n    * 支持在操作对象之间依赖关系，方便控制执行顺序。\n    * 支持可选的完成块，它在操作的主要任务完成后执行。\n    * 支持使用KVO通知监视操作执行状态的变化。\n    * 支持设定操作的优先级，从而影响它们的相对执行顺序。\n    * 支持取消操作，允许您在操作执行时暂停操作。\n  * 缺点：高级抽象，性能方面相较 GCD 来说不足一些;\n\n## `GCD`有哪些队列，默认提供哪些队列\n\n> [Grand Central Dispatch(GCD) 深入浅出](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F8cb4f395d2c4)\n\n| 队列                                               | 队列类型     | 说明                                                         |\n| -------------------------------------------------- | ------------ | ------------------------------------------------------------ |\n| 主队列（main queue）                               | 串行         | 保证所有的任务都在主线程执行，而主线程是唯一用于 UI 更新的线程。此外还用于发送消息给视图或发送通知。 |\n| 四个全局调度队列（high、default、low、background） | 并发         | Apple 的接口也会使用这些队列，所以你添加的任何任务都不会是这些队列中唯一的任务 |\n| 自定义队列                                         | 串行 or 并发 | 1. 多个任务以串行方式执行，但又不想在主线程中；2. 多个任务以并行方式执行，但不希望队列中有其他系统的任务干扰。 |\n\n## `GCD`有哪些方法api\n\n```objective-c\n\u002F\u002F 接口过多，罗列几个关键的\n\u002F\u002F 1. dispatch_sync\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    \u002F\u002F 同步 \n    dispatch_sync(\u003C#dispatch_queue_t  _Nonnull queue#>, \u003C#^(void)block#>)\n    \n    \u002F\u002F 由于是同步，线程A会被阻塞 \n    [self doOtherThing];\n}\n\n\u002F\u002F 2. dispatch_async\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    \u002F\u002F 异步 \n    dispatch_async(\u003C#dispatch_queue_t  _Nonnull queue#>, \u003C#^(void)block#>)\n    \n    \u002F\u002F 由于是异步，线程A不会被阻塞\n    [self doOtherThing];\n}\n\n\u002F\u002F 3. dispatch_after\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(\u003C#delayInSeconds#> * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{\n        \u003C#code to be executed after a specified delay#>\n    });\n}\n\n\u002F\u002F 4. dispatch_once\nstatic dispatch_once_t onceToken;\ndispatch_once(&onceToken, ^{\n    \u003C#code to be executed once#>\n});\n\n\u002F\u002F 5. dispatch_barrier_sync 和 dispatch_barrier_async\n\u002F\u002F 这个可以实现读写锁\n\u002F\u002F 线程 A 调用 someMethod 方法\n- (void)someMethod {   \n    \u002F\u002F 同步 \n    dispatch_barrier_sync(\u003C#dispatch_queue_t  _Nonnull queue#>, \u003C#^(void)block#>)\n    \n    \u002F\u002F 由于是同步，线程A会被阻塞 \n    [self doOtherThing];\n}\n\n\u002F\u002F 6. dispatch_apply\n\u002F\u002F 串行方式做事情\n- (void)serialDoSomething {\n  for(int idx=0; idx \u003C 3; idx++) {\n    \u002F\u002F 这里你可以处理事情 比如下载图片\n    downloadPic(idx);\n  }\n}\n\n\u002F\u002F 并发方式做事情\n- (void)concurrencyDoSomething {\n  dispatch_apply(3, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t idx) {\n        \u002F\u002F 由于下载图片之间没有任何关系，允许并发的去下载\n        downloadPic(idx);\n    })  \n}\n```\n\n7. Dispatch Groups 集合\n\n这里引入“**组(group)**”的概念，与队列不同，任何加入到组中的任务(task)，可以是串行执行或并行执行，可以来自任何其他队列，当组中所有任务完成之时，会通知你这个消息。下面是几个常用接口：\n\n- `dispatch_group_t group_name = dispatch_group_create();` 实例化一个组（就是个信号量）\n- `dispatch_group_enter(\u003C#dispatch_group_t _Nonnull group#>)` 和 `dispatch_group_leave(\u003C#dispatch_group_t _Nonnull group#>)` ，“加入”和“离开”是一对，就好比Objective-C 内存管理一样，谁持有(`retain`)谁释放(`release`)\n- `dispatch_group_wait(\u003C#dispatch_group_t _Nonnull group#>,DISPATCH_TIME_FOREVER)` 阻塞当前线程，等待任务组中的所有任务执行完毕。\n- `dispatch_group_notify(\u003C#dispatch_group_t _Nonnull group#>, \u003C#dispatch_queue_t _Nonnull queue#>, \u003C#^(void)block#>)` 和3不同，当组中的全部执行完毕，将 `block` 任务加入到队列 `queue` 执行。\n\n8. Semaphores 信号量\n\n```objective-c\ndispatch_group_t group = dispatch_group_create();   \u002F\u002F 1\ndispatch_semaphore_t semaphore = dispatch_semaphore_create(10);   \u002F\u002F 2\ndispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); \u002F\u002F 3  \nfor (int i = 0; i \u003C 100; i++)   \n{   \n    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);   \u002F\u002F 4 \n    dispatch_group_async(group, queue, ^{   \n        NSLog(@\"%i\",i);    \u002F\u002F 5\n        sleep(2); \n        dispatch_semaphore_signal(semaphore);   \u002F\u002F 6\n    });   \n}   \ndispatch_group_wait(group, DISPATCH_TIME_FOREVER);\u002F\u002F 7\nNSLog(@\"所有任务完成\");\n```\n\n## `GCD`主线程 & 主队列的关系\n\n队列其实就是一个数据结构体，主队列由于是串行队列，所以入队列中的 task 会逐一派发到主线程中执行；但是其他队列也可能会派发到主线程执行\n\n## 如何实现同步，有多少方式就说多少\n\n1. dispatch_sync\n2. dispatch_group，\n3. dispatch_semaphore\n4. NSLock\n5. pthread_mutex_t 互斥锁、递归锁等\n6.  @synchronized\n\n## `dispatch_once`实现原理\n\n```c\nvoid\ndispatch_once(dispatch_once_t *val, dispatch_block_t block)\n{\n\tdispatch_once_f(val, block, _dispatch_Block_invoke(block));\n}\n\nvoid\ndispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)\n{\n#if !DISPATCH_ONCE_INLINE_FASTPATH\n\tif (likely(os_atomic_load(val, acquire) == DLOCK_ONCE_DONE)) {\n\t\treturn;\n\t}\n#endif \u002F\u002F !DISPATCH_ONCE_INLINE_FASTPATH\n\treturn dispatch_once_f_slow(val, ctxt, func);\n}\n\nstatic void\ndispatch_once_f_slow(dispatch_once_t *val, void *ctxt, dispatch_function_t func)\n{\n\t_dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;\n\tstruct _dispatch_once_waiter_s dow = { };\n\t_dispatch_once_waiter_t tail = &dow, next, tmp;\n\tdispatch_thread_event_t event;\n\n\tif (os_atomic_cmpxchg(vval, NULL, tail, acquire)) {\n\t\tdow.dow_thread = _dispatch_tid_self();\n\t\t_dispatch_client_callout(ctxt, func);\n\n\t\tnext = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);\n\t\twhile (next != tail) {\n\t\t\ttmp = (_dispatch_once_waiter_t)_dispatch_wait_until(next->dow_next);\n\t\t\tevent = &next->dow_event;\n\t\t\tnext = tmp;\n\t\t\t_dispatch_thread_event_signal(event);\n\t\t}\n\t} else {\n\t\t_dispatch_thread_event_init(&dow.dow_event);\n\t\tnext = *vval;\n\t\tfor (;;) {\n\t\t\tif (next == DISPATCH_ONCE_DONE) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tif (os_atomic_cmpxchgv(vval, next, tail, &next, release)) {\n\t\t\t\tdow.dow_thread = next->dow_thread;\n\t\t\t\tdow.dow_next = next;\n\t\t\t\tif (dow.dow_thread) {\n\t\t\t\t\tpthread_priority_t pp = _dispatch_get_priority();\n\t\t\t\t\t_dispatch_thread_override_start(dow.dow_thread, pp, val);\n\t\t\t\t}\n\t\t\t\t_dispatch_thread_event_wait(&dow.dow_event);\n\t\t\t\tif (dow.dow_thread) {\n\t\t\t\t\t_dispatch_thread_override_end(dow.dow_thread, val);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t\t_dispatch_thread_event_destroy(&dow.dow_event);\n\t}\n}\n```\n\n上面是 libdispatch-913.60.2 的实现，稍显复杂，所以找了下旧版本的[其他大佬的解答摘抄下](http:\u002F\u002Flingyuncxb.com\u002F2018\u002F02\u002F01\u002FGCD源码分析2%20——%20dispatch-once篇\u002F)：\n\n```c\nvoid dispatch_once_f(dispatch_once_t *val, void *ctxt, void (*func)(void *)){\n    \n    volatile long *vval = val;\n    if (dispatch_atomic_cmpxchg(val, 0l, 1l)) {\n        func(ctxt); \u002F\u002F block真正执行\n        dispatch_atomic_barrier();\n        *val = ~0l;\n    } \n    else \n    {\n        do\n        {\n            _dispatch_hardware_pause();\n        } while (*vval != ~0l);\n        dispatch_atomic_barrier();\n    }\n}\n```\n\n- 1、在开篇中已经讲过`dispatch_atomic_cmpxchg`，它是一个宏定义，原型为`__sync_bool_compare_and_swap((p), (o), (n))` ，这是LockFree给予CAS的一种原子操作机制，原理就是 **如果p==o，那么将p设置为n，然后返回true;否则，不做任何处理返回false**( 博主这里小声逼逼：其实就是gcc提供的底层原子操作，CPU提供了在指令执行期间对总线加锁的手段，CPU芯片有一个引脚只要拉低电平就可以组织其他指令进行通过总线访问内存了，只有当前面的值指令执行完毕，把引脚电平复位就又恢复正常，这个就是硬件上的实现。)\n- 2、在多线程环境中，如果某一个线程A首次进入`dispatch_once_f`，*val==0，这个时候直接将其原子操作设为1，然后执行传入`dispatch_once_f`的block，然后调用`dispatch_atomic_barrier`，最后将*val的值修改为~0。\n- 3、`dispatch_atomic_barrier`是一种内存屏障，所谓内存屏障，从处理器角度来说，是用来串行化读写操作的，从软件角度来讲，就是用来解决顺序一致性问题的。编译器不是要打乱代码执行顺序吗，处理器不是要乱序执行吗，你插入一个内存屏障，就相当于告诉编译器，屏障前后的指令顺序不能颠倒，告诉处理器，只有等屏障前的指令执行完了，屏障后的指令才能开始执行。所以这里`dispatch_atomic_barrier`能保证只有在block执行完毕后才能修改*val的值。\n- 4、在首个线程A执行block的过程中，如果其它的线程也进入`dispatch_once_f`，那么这个时候if的原子判断一定是返回false，于是走到了else分支，于是执行了do~while循环，其中调用了`_dispatch_hardware_pause`，这有助于提高性能和节省CPU耗电，pause就像nop，干的事情就是延迟空等的事情。直到首个线程已经将block执行完毕且将*val修改为~0，调用`dispatch_atomic_barrier`后退出。这么看来其它的线程是无法执行block的，这就保证了在`dispatch_once_f`的block的执行的唯一性，生成的单例也是唯一的。\n\ndispatch_once死锁\n\n- 死锁方式1：\n  1、某线程T1()调用单例A，且为应用生命周期内首次调用，需要使用dispatch_once(&token, block())初始化单例。\n  2、上述block()中的某个函数调用了dispatch_sync_safe，同步在T2线程执行代码\n  3、T2线程正在执行的某个函数需要调用到单例A，将会再次调用dispatch_once。\n  4、这样T1线程在等block执行完毕，它在等待T2线程执行完毕，而T2线程在等待T1线程的dispatch_once执行完毕，造成了相互等待，故而死锁\n- 死锁方式2：\n  1、某线程T1()调用单例A，且为应用生命周期内首次调用，需要使用dispatch_once(&token, block())初始化单例；\n  2、block中可能掉用到了B流程，B流程又调用了C流程，C流程可能调用到了单例A，将会再次调用dispatch_once；\n  3、这样又造成了相互等待。\n\n所以在使用写单例时要注意：\n\n- 1、初始化要尽量简单，不要太复杂；\n- 2、尽量能保持自给自足，减少对别的模块或者类的依赖；\n- 3、单例尽量考虑使用场景，不要随意实现单例，否则这些单例一旦初始化就会一直占着资源不能释放，造成大量的资源浪费。\n\n> [**深入浅出 GCD 之 dispatch_once**](https:\u002F\u002Fxiaozhuanlan.com\u002Ftopic\u002F7916538240)\n\n## 什么情况下会死锁\n\n线程死锁是指由于两个或者多个线程互相持有对方所需要的资源，导致这些线程处于等待状态，无法前往执行。当线程互相持有对方所需要的资源时，会互相等待对方释放资源，如果线程都不主动释放所占有的资源，将产生死锁。\n\n\n\n## 有哪些类型的线程锁，分别介绍下作用和使用场景\n\n[[iOS] 谈谈iOS多线程的锁](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5a0a92996fb9a0451f307479) 一文直接吃透所有锁。\n\n## `NSOperationQueue`中的`maxConcurrentOperationCount`默认值\n\n默认值为 -1，默认的最大操作数由NSOperationQueue对象根据当前系统条件动态确定。\n\n## `NSTimer、CADisplayLink、dispatch_source_t` 的优劣\n\n### NSTimer\n\n方式一：\n\n> 接口文档：Creates a timer and schedules it on the current run loop in the default mode.\n\n```objective-c\nself.timer = [NSTimer scheduledTimerWithTimeInterval:2\n              target:self\n              selector:@selector(timerTest)\n              userInfo:nil\n              repeats:YES];\n\u002F\u002F 我们可以通过如下接口添加到commonMode中\n[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];\n```\n\n方式二：\n\n> You must add the new timer to a run loop, using [addTimer:forMode:](apple-reference-documentation:\u002F\u002FhcocJkO-uk). Then, after `ti` seconds have elapsed, the timer fires, sending the message `aSelector` to `target`. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)\n\n```objective-c\nself.timer = [NSTimer timerWithTimeInterval:2\n              target:self\n              selector:@selector(timerTest)\n              userInfo:nil\n              repeats:YES];\n[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];\n```\n\n> 关于 NSTimer 的 retainCycle 问题和解决方案可参考其他文章。解决方案有：\n>\n> 1.  NSTimer 加一个 category，然后以 block 的方式注入定时器触发时的执行任务，Timer 的 target 此处是用 Timer 类对象，而它是常驻内存中的，所以vc->timer->Timer类对象 没有构成环，但是注意闭包中要用 weakSelf；\n> 2.  封装一个中间对象 WeakProxy， 内部使用一个 weak 属性变量持有 self，所以现在持有关系式 vc->timer->weakProxy ---->vc，所以也没有形成 retainCycle。\n>\n> NSTimer repeats 等于 NO 的时候，执行完任务，定时器自动 invalidated 就会释放对 self 的 strong reference ，\n\n销毁的时候使用：\n\n```objective-c\n\u002F\u002F\u002F 文档接口说明：The timer maintains a strong reference to this object until it (the timer) is invalidated.\n[self.timer invalidate];\nself.timer = nil;\n```\n\n**缺点**\n\n**计时不精确**：不管是一次性的还是周期性的timer的实际触发事件的时间，都会与所加入的**RunLoop**和**RunLoop Mode**有关，如果此**RunLoop**正在执行一个连续性的运算，**timer**就会被延时出发。重复性的**timer**遇到这种情况，如果延迟超过了一个周期，则会在延时结束后立刻执行，并按照之前指定的周期继续执行。\n\n### CADisplayLink\n\n> Apple 专门提供的一个类，主要的优势在于他的执行频率是根据设备屏幕的刷新频率来计算的，也即是时间间隔最准确的定时器。用法和 NSTimer 差不多，当然也存在 retainCycle 问题，解决方式同上。\n\n暂停继续：\n\n```objective-c\n- (void)displayLinkStart {\n    self.displayLink.paused = !self.displayLink.paused;\n}\n```\n\n#### 优缺点\n\n- **优点：** 依托于设备屏幕刷新频率触发事件，所以其触发时间上是最准确的。也是最适合做UI不断刷新的事件，过渡相对流畅，无卡顿感。\n- **缺点：**\n  1. 由于依托于屏幕刷新频率，若果CPU不堪重负而影响了屏幕刷新，那么我们的触发事件也会受到相应影响。\n  2. selector触发的时间间隔只能是duration的整倍数\n  3. selector事件如果大于其触发间隔就会造成掉帧现象。\n\n### dispatch_source_t\n\n```objective-c\n- (void)createGCDTimer {\n    \u002F\u002F\u002F 创建定时器\n    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());\n    \u002F\u002F\u002F 设置触发间隔时间\n    dispatch_source_set_timer(self.timer, dispatch_walltime(NULL,0 * NSEC_PER_SEC), 1 * NSEC_PER_SEC, 0);\n    \n  \t\u002F\u002F\u002F 设置回调\n    dispatch_source_set_event_handler(self.timer, ^{\n        NSLog(@\"triggered\");\n    });\n}\n```\n\n**参数：**\n\n**dispatch_source_create()相关参数**\n\n|  参数  | 意义                                                       |\n| :----: | ---------------------------------------------------------- |\n|  type  | dispatch源可处理的事件                                     |\n| handle | 可以理解为句柄、索引或id，假如要监听进程，需要传入进程的ID |\n|  mask  | 可以理解为描述，提供更详细的描述，让它知道具体要监听什么   |\n| queue  | 自定义源需要的一个队列，用来处理所有的响应句柄（block）    |\n\n**dispatch_source_set_timer()相关参数**\n\n|   参数   | 意义                   |\n| :------: | ---------------------- |\n|  source  | dispatch_source_t      |\n|  start   | 事件首次触发的延迟时间 |\n| interval | 时间间隔               |\n|  leeway  | 误差范围               |\n\n#### 开始 暂停\n\n```objectivec\n\u002F\u002F开始\ndispatch_resume(self.disTimer);\n\u002F\u002F暂停\ndispatch_suspend(self.disTimer);\n```\n\n#### 销毁\n\n```css\ndispatch_source_cancel(self.disTimer);\n```\n\n**注意：**dispatch_source_t 一定要被设置为成员变量，否则将会立即被释放。\n\n#### 优缺点\n\n- **优点：**不受当前runloopMode的影响;\n- **缺点：**虽然不受runloopMode的影响，但是其计时效应仍不是百分之百准确的;\n\n# 六、视图&图像相关\n\n## 1. AutoLayout的原理，性能如何\n\n参考文章：[AutoLayout 的原理性能](https:\u002F\u002Fwww.dazhuanlan.com\u002F2019\u002F10\u002F05\u002F5d97e8f15427d\u002F)\n\n## 2. UIView & CALayer的区别\n\n* UIView 为 CALayer 提供内容，以及负责处理触摸等事件，参与响应链；\n\n* CALayer 负责显示内容 contents\n* 单一职责原则\n\n## 3. 事件响应链\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_c4758bef1002.png)\n\n>  慕尚课程的总结图\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_b4019fc64410.png)\n\n\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_b532e821caf4.png)\n\n## drawrect & layoutsubviews调用时机\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_74938b52bf6b.png)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_2d84d849c004.png)\n\n\n\n### layoutSubviews\n\n>  参考文章：[layoutSubviews和drawRect调用时机的探究](https:\u002F\u002Fblog.csdn.net\u002Fwangyanchang21\u002Farticle\u002Fdetails\u002F50774522)\n\n1. init初始化不会触发layoutSubviews。\n2. addSubview会触发layoutSubviews。\n3. 改变一个UIView的Frame会触发layoutSubviews，当然前提是frame的值设置前后发生了变化。\n4. 滚动一个UIScrollView引发UIView的重新布局会触发layoutSubviews。\n5. 旋转Screen会触发父UIView上的layoutSubviews事件。\n6. 直接调用 setNeedsLayout 或者 layoutIfNeeded。\n\n* `setNeedsLayout`\n  标记为需要重新布局，异步调用`layoutIfNeeded`刷新布局，不立即刷新，在下一轮runloop结束前刷新，对于这一轮`runloop`之内的所有布局和UI上的更新只会刷新一次，`layoutSubviews`一定会被调用。\n\n* `layoutIfNeeded`\n  如果有需要刷新的标记，立即调用`layoutSubviews`进行布局（如果没有标记，不会调用`layoutSubviews`）。\n\n\n\n## 4. 隐式动画 & 显示动画区别\n\n> 解答出自 [iOS动画-CALayer隐式动画原理与特性](https:\u002F\u002Fcloud.tencent.com\u002Fdeveloper\u002Farticle\u002F1418000)。\n\n隐式动画，指我们可以在不设定任何动画类型的情况下，仅仅改变CALayer的一个可做动画的属性，就能实现动画效果。\n\n### 1. 事务\n\n**事务**，其实是Core Animation用来包含一系列属性动画集合的机制，通过指定事务来改变图层的可动画属性，这些变化都不是立刻发生变化的，而是在事务被提交的时候才启动一个动画过渡到新值。任何可以做动画的图层属性都会被添加到栈顶的事务。\n\n```objective-c\n\u002F\u002F1.动画属性的入栈\n+ (void)begin;\n\n\u002F\u002F2.动画属性出栈\n+ (void)commit;\n\n\u002F\u002F3.设置当前事务的动画时间\n+ (void)setAnimationDuration:(CFTimeInterval)dur;\n\n\u002F\u002F4.获取当前事务的动画时间\n+ (CFTimeInterval)animationDuration;\n\n\u002F\u002F5.在动画结束时提供一个完成的动作\n+ (void)setCompletionBlock:(nullable void (^)(void))block;\n```\n\n现在再来考虑隐式动画，其实是Core Animation在每个RunLoop周期中会自动开始一次新的事务，即使你不显式的使用[CATranscation begin]开始一次事务，任何在一次RunLoop运行时循环中属性的改变都会被集中起来，执行默认0.25秒的动画。\n\n通过事务来设置动画：\n\n```objective-c\n[CATransaction begin];  \u002F\u002F入栈\n\u002F\u002F1.设置动画执行时间\n[CATransaction setAnimationDuration:3];\n\u002F\u002F2.设置动画执行完毕后的操作：颜色渐变之后再旋转90度\n[CATransaction setCompletionBlock:^{\n  CGAffineTransform transform = self.colorLayer.affineTransform;\n  transform  = CGAffineTransformRotate(transform, M_PI_2);\n  self.colorLayer.affineTransform = transform;\n}];\n\nCGFloat red = arc4random() % 255 \u002F 255.0;\nCGFloat green = arc4random() % 255 \u002F 255.0;\nCGFloat blue = arc4random() % 255 \u002F 255.0;\nUIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];\n_colorLayer.backgroundColor = randomColor.CGColor;\n[CATransaction commit];  \u002F\u002F出栈\n```\n\n### 2. 图层行为\n\n我们上述的实验对象是一个独立图层，如果直接对UIView或者CALayer关联的图层layer改变动画属性，这样是没有隐式动画效果的，这说明虽然Core Animation对所有的CALayer动画属性设置了隐式动画，但UIView把它关联的图层的这个特性给关闭了。 为了更好的理解中一点，我们需要知道隐式动画是如何实现的： 我们把改变属性时CALayer自动执行的动画称作行为，当CALayer的属性被修改时，它会调用-actionForKey:方法传递属性名称，我们可以找到这个方法的具体说明如下：\n\n```objective-c\n\u002F* Returns the action object associated with the event named by the\n * string 'event'. The default implementation searches for an action\n * object in the following places:\n *\n * 1. if defined, call the delegate method -actionForLayer:forKey:\n * 2. look in the layer's `actions' dictionary\n * 3. look in any `actions' dictionaries in the `style' hierarchy\n * 4. call +defaultActionForKey: on the layer's class\n *\n * If any of these steps results in a non-nil action object, the\n * following steps are ignored. If the final result is an instance of\n * NSNull, it is converted to `nil'. *\u002F\n\n- (nullable id\u003CCAAction>)actionForKey:(NSString *)event;\n```\n\n翻译过来大概就是说：\n\n1. 图层会首先检测它是否有委托，并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法；如果有，就直接调用并返回结果。\n2. 如果没有委托或者委托没有实现-actionForLayer:forKey方法，图层将会检查包含属性名称对应行为映射的actions字典\n3. 如果actions字典没有包含对应的属性，图层接着在它的style字典里搜索属性名.\n4. 最后，如果在style也找不到对应的行为，那么图层将会直接调用定义了每个属性的标准行为的+defaultActionForKey:方法\n\n从流程上分析来看，经过一次完整的搜索动画之后，-actionForKey:要么返回空(这种情况不会有动画发生)，要么返回遵循CAAction协议的对象(CALayer拿这个结果去对先前和当前的值做动画)。现在我们再来考虑UIKit是如何禁用隐式动画的： 每个UIView对它关联的图层都遵循了CALayerDelegate协议，并且实现了-actionForLayer:forKey方法。当不在一个动画块中修改动画属性时，UIView对所有图层行为都返回了nil，但是在动画Block范围就返回了非空值，下面通过一段代码来验证：\n\n```objective-c\n@interface TestLayerAnimationVC ()\n\n@property (nonatomic,weak)IBOutlet UIView *layerView;\n\n@end\n\n- (void)viewDidLoad {\n    [super viewDidLoad];\n   \u002F\u002F测试图层行为：UIKit默认关闭了自身关联图层的隐式动画\n    NSLog(@\"OutSide:%@\",[self.layerView actionForLayer:self.layerView.layer forKey:@\"backgroundColor\"]);\n   \n    [UIView beginAnimations:nil context:nil];\n    NSLog(@\"InSide:%@\",[self.layerView actionForLayer:self.layerView.layer forKey:@\"backgroundColor\"]);\n    [UIView commitAnimations];\n}\n\n\u002F\u002F打印：\nOutSide:\u003Cnull>\nInSide:\u003CCABasicAnimation: 0x600001703100>\n```\n\n由此得出结论：当属性在动画块之外发生变化，UIView直接通过返回nil来禁用隐式动画。但是如果在动画块范围内，UIView则会根据动画具体类型返回响应的属性，\n\n### 3. 关闭和开启隐式动画\n\n当然，返回nil并不是禁用隐式动画的唯一方法，CATransaction也为我们提供了具体的方法，可以用来对所有属性打开或者关闭隐式动画，方法如下：\n\n```javascript\n+ (void)setDisableActions:(BOOL)flag;\n```\n\nUIView关联的图层禁用了隐式动画，那么对这种图层做动画的方法有有了以下几种方式：\n\n1. 使用UIView的动画函数(而不是依赖CATransaction)\n2. 继承UIView，并覆盖-actionforLayer:forkey:方法\n3. 直接创建显式动画\n\n其实，对于单独存在的图层，我们也可以通过实现图层的-actionforLayer:forkey:方法，或者提供一个actions字典来控制隐式动画\n\n### 4. 自定义层行为\n\n通过了解事务和图层行为，我们可以这样理解：图层行为实际上是 Core Animation 隐式调用的显式动画对象。我们可以通过两种方式来改变这种隐式的图层行为：1. 为图层设置自定义的 actions 字典；2. 实现委托代理，返回遵循 CAAction 协议的动画对象。现在，我们尝试使用第一种方法来自定义图层行为，这里用到的是一个推进过渡的动画（同样遵循 CAAction 协议的动画类），具体的代码如下：\n\n```javascript\n@interface TestLayerAnimationVC ()\n@property (nonatomic,strong) CALayer *colorLayer;\n@end\n\n- (void)viewDidLoad {\n    [super viewDidLoad];\n\n    _colorLayer = [[CALayer alloc] init];\n    _colorLayer.frame = CGRectMake(30, 30, kDeviceWidth - 60, 60);\n    _colorLayer.backgroundColor = [UIColor orangeColor].CGColor;\n    \u002F\u002F自定义动画对象\n    CATransition *transition = [CATransition animation];\n    transition.type = kCATransitionPush;\n    transition.subtype = kCATransitionFromLeft;\n    _colorLayer.actions = @{@\"backgroundColor\":transition};\n    [self.view.layer addSublayer:_colorLayer];\n}\n\n- (IBAction)changeColor:(UIButton *)sender{\n    CGFloat red = arc4random() % 255 \u002F 255.0;\n    CGFloat green = arc4random() % 255 \u002F 255.0;\n    CGFloat blue = arc4random() % 255 \u002F 255.0;\n    UIColor *randomColor = [UIColor colorWithRed:red green:green blue:blue alpha:1];\n    _colorLayer.backgroundColor = randomColor.CGColor;\n}\n```\n\n\n\n## 5. 什么是离屏渲染\n\n[离屏渲染专题](https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record\u002Ftree\u002Fmaster\u002Ftopics\u002F离屏渲染专题)\n\n## 6. imageNamed & imageWithContentsOfFile区别\n\niOS 加载本地图片有两种方式：imageNamed 和 imageWithContentOfFile。\n\n#### imageNamed\n\n前者 Apple 官方文档有提到：\n\n> 此方法会在系统缓存中查找具有指定名称的图像对象，如果存在则返回该对象。如果缓存中尚未存在匹配的图像对象，则此方法会从磁盘或资源目录中定位并加载图像数据，然后返回结果。请注意，此方法并非线程安全。\n\n首先从系统缓存中根据图片名称寻找图片，如果找到了就返回。如果没有在缓存中找到图片，该方法会从指定的文件中加载图片数据，并将其缓存起来，然后再把结果返回，下次再使用该名称图片的时候就省去了从硬盘中加载图片的过程。对于相同名称的图片，**系统只会把它 Cache 到内存一次，**如果相应的图片数据不存在，返回 nil。\n\n关于加载：`imageNamed` 方法可以加载 `Assets.xcassets` 和 bundle 中的图片。\n\n关于缓存：加载到内存当中会一直存在内存当中，（图片）不会随着对象的销毁而销毁；加载进去图片后，占用的内存归系统管理，我们是无法管理的；相同的图片是不会重复加载的，加载到内存中占据的内存较大。\n\n```objective-c\nUIImage *img = [UIImage imageNamed:@\"pic\"];\n```\n\n#### imageWithContentsOfFile\n\n`imageWithContentsOfFile` 方法只是简单的加载图片，并不会将图片缓存起来，图像会被系统以数据方式加载到程序。\n\n关于加载：`imageWithContentsOfFile` 只能加载 mainBundle 中图片。\n\n关于缓存：加载到内存中占据的内存较小，相同的图片会被重复加载到内存当中，加载的图片会随着对象的销毁而销毁；\n\n#### 两者应用场景\n\n* 如果图片较小，并且使用频繁的图片使用 imageNamed：方法来加载\n* 如果图片较大，并且使用较少，使用imageWithContentOfFile:来加载。\n* 当你不需要重用该图像，或者你需要将图像以数据方式存储到数据库，又或者你要通过网络下载一个很大的图像时，使用 `imageWithContentsOfFile`；\n* 如果在程序中经常需要重用的图片，比如用于 UITableView 的图片，那么最好是选择 imageNamed 方法。这种方法可以节省出每次都从磁盘加载图片的时间；\n\n> *  [《iOS之图片加载》](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002Fea2a2ba3cd97)\n> *  [IOS如何选择图片加载方式：imageNamed和imageWithContentsOfFile的区别](https:\u002F\u002Fblog.csdn.net\u002Fwzzvictory\u002Farticle\u002Fdetails\u002F9053813)\n\n## 7. 多个相同的图片，会重复加载吗\n\n答案见上\n\n## 8. 图片是什么时候解码的，如何优化\n\n* [iOS 图片解码（decode）笔记](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F4da6981a746c)\n* [探讨iOS 中图片的解压缩到渲染过程](https:\u002F\u002Fwww.jianshu.com\u002Fp\u002F72dd074728d8)\n\n## 9. 图片渲染怎么优化\n\n> 出自 swift.gg 文章 [图像渲染优化技巧](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F)\n\n####9.1 图片尺寸**明显大于** **`UIImageView`** 显示尺寸的场景\n\n1. [绘制到 UIGraphicsImageRenderer 上](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-1-drawing-to-a-uigraphicsimagerenderer)\n\n   ```swift\n   import UIKit\n   \n   \u002F\u002F 技巧 #1\n   func resizedImage(at url: URL, for size: CGSize) -> UIImage? {\n       guard let image = UIImage(contentsOfFile: url.path) else {\n           return nil\n       }\n   \n       let renderer = UIGraphicsImageRenderer(size: size)\n       return renderer.image { (context) in\n           image.draw(in: CGRect(origin: .zero, size: size))\n       }\n   }\n   ```\n\n   [`UIGraphicsImageRenderer`](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fuikit\u002Fuigraphicsimagerenderer) 是一项相对较新的技术，在 iOS 10 中被引入，用以取代旧版本的 `UIGraphicsBeginImageContextWithOptions` \u002F `UIGraphicsEndImageContext` API。你通过指定以 `point` 计量的 `size` 创建了一个 `UIGraphicsImageRenderer`。`image` 方法带有一个闭包参数，返回的是一个经过闭包处理后的位图。最终，原始图像便会在缩小到指定的范围内绘制。\n\n   > 在不改变图像原始纵横比（aspect ratio）的情况下，缩小图像原始的尺寸来显示通常很有用。[`AVMakeRect(aspectRatio:insideRect:)`](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Favfoundation\u002F1390116-avmakerect) 是在 AVFoundation 框架中很方便的一个函数，负责帮你做如下的计算：\n\n   ```\n   import func AVFoundation.AVMakeRect\n   let rect = AVMakeRect(aspectRatio: image.size, insideRect: imageView.bounds)\n   ```\n\n2. [绘制到 Core Graphics Context 上](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-2-drawing-to-a-core-graphics-context)\n\n   ```swift\n   import UIKit\n   import CoreGraphics\n   \n   \u002F\u002F 技巧 #2\n   func resizedImage(at url: URL, for size: CGSize) -> UIImage? {\n       guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil),\n           let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)\n       else {\n           return nil\n       }\n   \n       let context = CGContext(data: nil,\n                               width: Int(size.width),\n                               height: Int(size.height),\n                               bitsPerComponent: image.bitsPerComponent,\n                               bytesPerRow: image.bytesPerRow,\n                               space: image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,\n                               bitmapInfo: image.bitmapInfo.rawValue)\n       context?.interpolationQuality = .high\n       context?.draw(image, in: CGRect(origin: .zero, size: size))\n   \n       guard let scaledImage = context?.makeImage() else { return nil }\n   \n       return UIImage(cgImage: scaledImage)\n   }\n   ```\n\n   \n\n3. [使用 Image I\u002FO 创建缩略图像](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-3-creating-a-thumbnail-with-image-io)\n\n   Image I\u002FO 是一个强大（却鲜有人知）的图像处理框架。抛开 Core Graphics 不说，它可以读写许多不同图像格式，访问图像的元数据，还有执行常规的图像处理操作。这个框架通过先进的缓存机制，提供了平台上最快的图片编码器和解码器，甚至可以增量加载图片。\n\n   这个重要的 `CGImageSourceCreateThumbnailAtIndex` 提供了一个带有许多不同配置选项的 API，比起在 Core Graphics 中等价的处理操作要简洁得多：\n\n4. [使用 Core Image 进行 Lanczos 重采样](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-4-lanczos-resampling-with-core-image)\n\n   Core Image 内置了 [Lanczos 重采样（resampling）](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FLanczos_resampling) 功能，它是以 `CILanczosScaleTransform` 的同名滤镜命名的。虽然可以说它是在 UIKit 层级之上的 API，但无处不在的 key-value 编写方式导致它使用起来很不方便。\n\n   即便如此，它的处理模式还是一致的。\n\n   创建转换滤镜，对滤镜进行配置，最后渲染输出图像，这样的步骤和其他任何 Core Image 的工作流没什么不同。\n\n   ```\n   import UIKit\n   import CoreImage\n   \n   let sharedContext = CIContext(options: [.useSoftwareRenderer : false])\n   \n   \u002F\u002F 技巧 #4\n   func resizedImage(at url: URL, scale: CGFloat, aspectRatio: CGFloat) -> UIImage? {\n       guard let image = CIImage(contentsOf: url) else {\n           return nil\n       }\n   \n       let filter = CIFilter(name: \"CILanczosScaleTransform\")\n       filter?.setValue(image, forKey: kCIInputImageKey)\n       filter?.setValue(scale, forKey: kCIInputScaleKey)\n       filter?.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)\n   \n       guard let outputCIImage = filter?.outputImage,\n           let outputCGImage = sharedContext.createCGImage(outputCIImage,\n                                                           from: outputCIImage.extent)\n       else {\n           return nil\n       }\n   \n       return UIImage(cgImage: outputCGImage)\n   }\n   ```\n\n5. [使用 vImage 优化图片渲染](https:\u002F\u002Fswift.gg\u002F2019\u002F11\u002F01\u002Fimage-resizing\u002F#technique-5-image-scaling-with-vimage)\n\n   最后一个了，它是古老的 [Accelerate 框架](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Faccelerate) —— 更具体点来说，它是 `vImage` 的图像处理子框架。\n\n   vImage 附带有 [一些不同的功能](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Faccelerate\u002Fvimage\u002Fvimage_operations\u002Fimage_scaling)，可以用来裁剪图像缓冲区大小。这些底层 API 保证了高性能同时低能耗，但会导致你对缓冲区的管理操作增加（更不用说要编写更多的代码了）\n\n- **UIKit**, **Core** **Graphics**, 和 **Image** **I\u002FO** 都能很好地用于大部分图片的优化操作。如果（在 iOS 平台，至少）要选择一个的话，`UIGraphicsImageRenderer` 是你最佳的选择。\n- **Core** **Image** 在图像优化渲染操作方面性能表现优越。实际上，根据 Apple 官方 [*Core* *Image* *编程规范中的性能最佳实践单元*](https:\u002F\u002Fdeveloper.apple.com\u002Flibrary\u002Fmac\u002Fdocumentation\u002Fgraphicsimaging\u002FConceptual\u002FCoreImaging\u002Fci_performance\u002Fci_performance.html#\u002F\u002Fapple_ref\u002Fdoc\u002Fuid\u002FTP30001185-CH10-SW1)，你应该使用 Core Graphics 或 Image I\u002FO 对图像进行裁剪和下采样，而不是用 Core Image。\n\n## 10. 如果GPU的刷新率超过了iOS屏幕60Hz刷新率是什么现象，怎么解决\n\n# 七、性能优化\n\n1. 如何做启动优化，如何监控\n2. 如何做卡顿优化，如何监控\n   1. FPS 用 CADisplayLinker 来计数\n   2. 监听 runloop 的 source0 事件和进入休眠前，然后设定一个阈值，超过几次算卡顿\n   3. ping 方案，起一个子线程，while(1) 每次 async 一个 task 到主线程进行标志位置位，然后休眠或者等待一定时间在子线程检查是否这个task被执行了。\n3. 如何做耗电优化，如何监控\n4. 如何做网络优化，如何监控\n\n> [iOS面试题：简述性能优化](https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F96963676)\n\n# 八、开发证书\n\n1. 苹果使用证书的目的是什么\n\n   在 iOS 平台对第三方 APP 有绝对的控制权，一定要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的，场景有如下三种\n\n   1. AppStore 下载应用验证，传 App 上 AppStore 时，苹果后台用私钥对 APP 数据进行签名，iOS 系统下载这个 APP 后，用公钥验证这个签名，若签名正确，这个 APP 肯定是由苹果后台认证的，并且没有被修改过，也就达到了苹果的需求：保证安装的每一个 APP 都是经过苹果官方允许的。\n   2. 开发 App 时可以直接把开发中的应用安装进手机进行调试。\n   3. In-House 企业内部分发，可以直接安装企业证书签名后的 APP。\n   4. AD-Hoc 相当等于企业分发的限制版，限制安装设备数量，较少用。\n\n2. AppStore安装app时的认证流程\n\n   略\n\n3. 开发者怎么在debug模式下把app安装到设备呢\n\n   略\n\n# 九、架构设计\n\n## 典型源码的学习\n\n只是列出一些iOS比较核心的开源库，这些库包含了很多高质量的思想，源码学习的时候一定要关注每个框架解决的核心问题是什么，还有它们的优缺点，这样才能算真正理解和吸收\n\n1. AFN\n2. SDWebImage\n3. JSPatch、Aspects(虽然一个不可用、另一个不维护，但是这两个库都很精炼巧妙，很适合学习)\n4. Weex\u002FRN, 笔者认为这种前端和客户端紧密联系的库是必须要知道其原理的\n5. CTMediator、其他router库，这些都是常见的路由库，开发中基本上都会用到\n6. 请`圈友`们在评论下面补充吧\n\n## 架构设计\n\n1. 手动埋点、自动化埋点、可视化埋点\n2. `MVC、MVP、MVVM`设计模式\n3. 常见的设计模式\n4. 单例的弊端\n5. 常见的路由方案，以及优缺点对比\n6. 如果保证项目的稳定性\n7. 设计一个图片缓存框架(LRU)\n8. 如何设计一个`git diff`\n9. 设计一个线程池？画出你的架构图\n10. 你的app架构是什么，有什么优缺点、为什么这么做、怎么改进\n\n# 十、其他问题\n\n1. `PerformSelector & NSInvocation`优劣对比\n2. `oc`怎么实现多继承？怎么面向切面（可以参考[Aspects深度解析-iOS面向切面编程](https:\u002F\u002Fjuejin.im\u002Fpost\u002F5e13c4366fb9a047f42e6406)）\n3. 哪些`bug`会导致崩溃，如何防护崩溃\n4. 怎么监控崩溃\n5. `app`的启动过程（考察LLVM编译过程、静态链接、动态链接、runtime初始化）\n6. 沙盒目录的每个文件夹划分的作用\n7. 简述下`match-o`文件结构\n\n# 十一、系统基础知识\n\n### 进程和线程的区别\n\n> 关于这个问题的解答有很多，这里暂时引用自[进程和线程的区别介绍](https:\u002F\u002Fbaijiahao.baidu.com\u002Fs?id=1611925141861592999&wfr=spider&for=pc)一文。\n\n1、首先是定义\n\n进程：是执行中一段程序，即一旦程序被载入到内存中并准备执行，它就是一个进程。进程是表示资源分配的的基本概念，又是调度运行的基本单位，是系统中的并发执行的单位。\n\n线程：单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。\n\n2、一个线程只能属于一个进程，但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。\n\n3、线程是一种轻量级的进程，与进程相比，线程给操作系统带来侧创建、维护、和管理的负担要轻，意味着线程的代价或开销比较小。\n\n4、线程没有地址空间，线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权，线程文本包含在他的进程 的文本片段中，进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量)，数据段(全局变量和静态变量)，扩展段(堆存储)。但是每个线程拥有自己的栈段， 寄存器的内容，栈段又叫运行时段，用来存放所有局部变量和临时变量。\n\n5、父和子进程使用进程间通信机制，同一进程的线程通过读取和写入数据到进程变量来通信。\n\n6、进程内的任何线程都被看做是同位体，且处于相同的级别。不管是哪个线程创建了哪一个线程，进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制，进程中任何线程都可以通过销毁主线程来销毁进程，销毁主线程将导致该进程的销毁，对主线程的修改可能影响所有的线程。\n\n7、子进程不对任何其他子进程施加控制，进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制，进程中所有线程都可以对主线程施加控制。\n\n相同点：\n\n进程和线程都有ID\u002F寄存器组、状态和优先权、信息块，创建后都可更改自己的属性，都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。\n\n### `HTTPS`的握手过程\n\n密钥协商过程：\n\n1. 客户端将TLS版本，支持的加密算法，ClientHello random C 发给服务端【客户端->服务端】\n2. 服务端从加密算法中pick一个加密算法， ServerHello random  S，server 证书返回给客户端；【服务端->客户单】\n3. 客户端验证 server 证书【客户端】\n4. 客户端生成一个 48 字节的预备主密钥，其中前2个字节是 Protocol Version，后46个字节是随机数，客户端用证书中的公钥对预备主密钥进行非对称加密后通过 client key exchange 子消息发给服务端【客户端->服务端】\n5. 服务端用私钥解密得到预备主密钥；【服务端】\n6. 服务端和客户端都可以通过预备主密钥、ClientHello random C 和  ServerHello random S 通过 PRF 函数生成主密钥；会话密钥由主密钥、SecurityParameters.server_random 和 SecurityParameters.client_random 数通过 PRF 函数来生成会话密钥里面包含对称加密密钥、消息认证和 CBC 模式的初始化向量，对于非 CBC 模式的加密算法来说，就没有用到这个初始化向量。\n\n> Session ID 缓存和 Session Ticket 里面保存的也是主密钥，而不是会话密钥，这样每次会话复用的时候再用双方的随机数和主密钥导出会话密钥，从而实现每次加密通信的会话密钥不一样，即使一个会话的主密钥泄露了或者被破解了也不会影响到另一个会话。\n\n### 什么是`中间人攻击`？怎么预防\n\nHTTP 明文传输，客户端和服务端进行通信时，中间人即指夹在客户端和服务端之间的第三者，对于客户端来说，中间人就是 **服务端**，对于服务端来说，中间人就是 **客户端**。中间人拦截客户端消息，然后再发送给服务端；服务端发发送消息给中间人，中间人再返还给客户端。\n\n使用 HTTPS，单双向认证，\n\n### `TCP`的握手过程？为什么进行三次握手，四次挥手\n\n三次握手：\n\n> 为了确认服务端和客户端双方的收发能力。\n\n* 客户端发送 SYN = 1，seq=x 给服务端\n* 服务端接收发送 SYN = 1，ACK = 1，ack=x+1， seq = y 给客户端\n* 客户端发送 ACK = 1，ack = y+1 ，seq = z 给服务端\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_6bc22c1f6c61.png)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_87b5ca5299f7.png)\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_54b291f831ea.png)\n\n四次挥手：\n\n* 主动方发送 FIN = 1，seq = u 给被动方；\n* 被动方 ACK = 1，ack = u+1，seq = v；\n* 被动方继续传输数据给主动方；\n* 被动方没有更多数据了，发送 FIN = 1，ACK=1，seq = w，ack=u+1；\n* 主动方 ACK = 1，seq = u + 1，ack = w +1;\n\n### `堆和栈`区的区别？谁的占用内存空间大\n\n> 解答出自：[堆区（heap）和栈区（stack）的区别](https:\u002F\u002Fblog.csdn.net\u002Fshanshanhi\u002Farticle\u002Fdetails\u002F50904706)\n\n（1）申请方式\n\n栈区：由编译器自动分配释放，存放函数的参数值，局部变量值等；\n\n堆区：一般由程序员分配释放（使用new\u002Fdelete或malloc\u002Ffree），若程序员不释放，程序结束时可能由OS回收；\n\n（2）操作方式\n\n栈区：操作方式类似于数据结构中的栈；\n\n堆区：不同于数据结构中的堆，分配方式类似于链表。\n\n（3）申请后系统的响应 \n\n栈区：只要栈的剩余空间大于所申请空间，系统将为程序提供内存，否则将报异常提示栈溢出；\n\n堆区：首先应该知道操作系统有一个记录空闲内存地址的链表，当系统收到程序的申请时，会遍历该链表，寻找第一个空间大于所申请空间的堆结点，然后将该结点从空闲结点链表中删除，并将该结点的空间分配给程序，另外，对于大多数系统，会在这块内存空间中的首地址处记录本次分配的大小，这样，代码中的delete语句才能正确的释放本内存空间。另外，由于找到的堆结点的大小不一定正好等于申请的大小，系统会自动的将多余的那部分重新放入空闲链表中。 \n\n（4）申请大小的限制\n\n栈区：在Windows下,栈是向低地址扩展的数据结构，是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的，在WINDOWS下，栈的大小是2M（也有的说是1M，总之是一个编译时就确定的常数），如果申请的空间超过栈的剩余空间时，将提示overflow。因此，能从栈获得的空间较小。\n\n堆区：堆是向高地址扩展的数据结构，是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的，自然是不连续的，而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见，堆获得的空间比较灵活，也比较大。 \n\n（5）申请效率的比较\n\n栈区：系统自动分配，速度较快。但程序员是无法控制的。\n\n堆区：由new分配的内存，一般速度比较慢，而且容易产生内存碎片,不过用起来最方便. \n\n注意：在WINDOWS下，最好的方式是用VirtualAlloc分配内存，他不是在堆，也不是在栈是直接在进程的地址空间中保留一快内存，虽然用起来最不方便。但是速度快，也最灵活。\n\n（6）堆和栈中的存储内容\n\n栈区：在函数调用时，第一个进栈的是主函数中后的下一条指令（函数调用语句的下一条可执行语句）的地址，然后是函数的各个参数，在大多数的C编译器中，参数是由右往左入栈的，然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后，局部变量先出栈，然后是参数，最后栈顶指针指向最开始存的地址，也就是主函数中的下一条指令，程序由该点继续运行。\n\n堆区：一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。\n\n### 加密算法：`对称加密算法和非对称加密算法`区别\n\n### 常见的`对称加密和非对称加密`算法有哪些\n\n#### 对称加密\n\n对称加密指的就是加密和解密使用同一个秘钥，所以叫做对称加密。对称加密只有一个秘钥，作为私钥。\n\n具体算法有：DES，3DES，TDEA，Blowfish，RC5，IDEA。常见的有：DES，AES，3DES等等。\n\n> 优点：算法公开、计算量小、加密速度快、加密效率高。 缺点：秘钥的管理和分发非常困难，不够安全。在数据传送前，发送方和接收方必须商定好秘钥，然后双方都必须要保存好秘钥，如果一方的秘钥被泄露，那么加密信息也就不安全了。另外，每对用户每次使用对称加密算法时，都需要使用其他人不知道的唯一秘钥，这会使得收、发双方所拥有的钥匙数量巨大，密钥管理成为双方的负担。\n\n#### 非对称加密\n\n非对称加密指的是：加密和解密使用不同的秘钥，一把作为公开的公钥，另一把作为私钥。公钥加密的信息，只有私钥才能解密。私钥加密的信息，只有公钥才能解密。 私钥只能由一方安全保管，不能外泄，而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密，而解密则需要另一个密钥。\n\n我们常见的数字证书、加密狗即是采用非对称加密来完成安全验证的。\n\n> 优点：安全性更高，公钥是公开的，秘钥是自己保存的，不需要将私钥给别人。 缺点：加密和解密花费时间长、速度慢，只适合对少量数据进行加密。\n\n主要算法：RSA、Elgamal、背包算法、Rabin、HD,ECC（椭圆曲线加密算法）。常见的有：RSA，ECC\n\n#### 银行动态令牌\n\n网银比较流行的时候，银行给我们发一个动态令牌。这个令牌并不使用任何对称或者非对称加密的算法，在整个银行的认证体系中，动态令牌只是一个一次性口令的产生器，它是基于时间同步方式，每隔60秒产生一个随机6位动态密码在其中运行的主要计算仅包括时间因子的计算和散列值的计算。\n\n在用户从银行手中拿到动态口令令牌卡的时候，在令牌卡的内部已经存储了一份种子文件（即图中钥匙所代表的seed），这份种子文件在银行的服务器里保存的完全一样的一份，所以对于动态口令令牌来说，这种方式是share secret的。另外在令牌硬件上的设置中，假使有人打开了这个令牌卡，种子文件将会从令牌卡的内存上擦除（待考证）。 令牌卡中有了种子文件，并实现了TOTP算法，在预先设置的间隔时间里它就能不断产生不同的动态口令，并显示到屏幕上，而银行服务器上跟随时间做同样的计算，也会得到和令牌卡同样的口令，用作认证。 那么TOTP算法具体做了什么操作呢？在RFC6238中有详细的算法描述，这里也会做简单的叙述。\n\n> TOTP是来自 HOTP [RFC4226] 的变形，从统筹上看，他们都是将数据文件进行散列计算，只是HOTP的因子是事件因子，TOTP将因子换成了时间因子，具体的TOTP计算公式(其中的HMAC-SHA-256也可能是 HMAC-SHA-512)： TOTP = Truncate(HMAC-SHA-256(K,T))\n\n其中： K 为这里的种子文件内容； T 为计算出来的时间因子 公式中的 HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)，HMAC运算利用哈希算法，以一个密钥和一个消息为输入，生成一个消息摘要作为输出。而公式中给出的哈希算法是 SHA-256，这种哈希算法目前并没有好的破解办法。 令牌卡中预先设置了要显示的口令长度，TOTP 中的 Truncate 操作剪切获得口令。 以上就是动态口令令牌卡的内部原理。\n\n> 解答出自 [对称加密算法与非对称加密算法的优缺点](https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F38307899)\n\n### `MD5、Sha1、Sha256`区别\n\n签名算法，SHA(Security Hash Algorithm) ，貌似 MD5 更高效，花费时间更少，但相对较容易**碰撞**。SHA1 已经被攻破，所以安全性不行。\n\n### `charles`抓包过程？不使用`charles`，`4G`网络如何抓包\n\n 中间人攻击原理，easy。\n\n# 十二、数据结构与算法\n\n>  pmst： 建议 [LeetCode](https:\u002F\u002Fleetcode.com\u002F)上刷题，面试是一方面，锻炼我们的*逻辑*思维、分析能力，就比如今天的每日一题[面试题 16.03. 交点](https:\u002F\u002Fleetcode-cn.com\u002Fproblems\u002Fintersection-lcci\u002F)，问题难度不高，就是初中的斜线问题，主要是分析多种情况下的交点，理顺逻辑很重要。\n\n1. 八大排序算法\n2. 栈&队列\n3. 字符串处理\n4. 链表\n5. 二叉树相关操作\n6. 深搜广搜\n7. 基本的动态规划题、贪心算法、二分查找\n\n> 如果你觉得这份资料对你有价值，欢迎 star，fork，☕️\n\n![](https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_readme_c06580f5f0cf.png)","# bytedance-alibaba-interview 快速上手指南\n\n本指南旨在帮助开发者快速搭建并阅读阿里、字节高效的 iOS 面试题解答源码及调试环境。该项目主要包含对 Objective-C Runtime 底层原理的深度解析与可运行示例。\n\n## 环境准备\n\n在开始之前，请确保您的开发环境满足以下要求：\n\n*   **操作系统**: macOS (推荐最新稳定版)\n*   **开发工具**: Xcode (建议版本 12.0 及以上，以支持较新的 Objective-C 运行时特性)\n*   **前置知识**: 熟悉 Objective-C 语言基础，了解基本的 iOS 开发流程\n*   **依赖项**: 无需额外安装第三方包管理器依赖，项目主要基于 Apple 开源的 `objc4` 源码进行注解和调试。\n\n## 安装步骤\n\n### 1. 克隆项目代码\n\n使用终端执行以下命令，将仓库克隆到本地：\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fcolourful987\u002Fbytedance-alibaba-interview.git\ncd bytedance-alibaba-interview\n```\n\n> **提示**：如果访问 GitHub 速度较慢，可使用国内镜像源（如 Gitee 镜像，若有）或通过代理加速下载。\n\n### 2. 获取 Runtime 源码（可选但推荐）\n\n为了深入理解文中提到的 `class_rw_t`、`class_ro_t` 等结构体，建议下载带注释的可运行源码或官方原始源码进行对照调试。\n\n*   **下载带注释的调试源码** (推荐):\n    ```bash\n    git clone https:\u002F\u002Fgithub.com\u002Fcolourful987\u002F2020-Read-Record.git\n    # 进入 objc4-750 注解源码目录\n    cd 2020-Read-Record\u002FAnnotated\\ source\\ code\u002Fobjc4-750\n    ```\n\n*   **或从 Apple 官网下载原始 Tarball**:\n    访问 [Apple Open Source - objc4](https:\u002F\u002Fopensource.apple.com\u002Ftarballs\u002Fobjc4\u002F) 下载对应版本源码包并解压。\n\n### 3. 打开项目\n\n在 Finder 中进入克隆后的目录，双击 `.xcodeproj` 或 `.xcworkspace` 文件（视具体子目录结构而定，通常位于 `demos` 文件夹下的测试工程）在 Xcode 中打开。\n\n例如，查看关于 `load` 方法加载顺序的测试 Demo：\n\n```bash\nopen demos\u002FTestLoad 方法\u002FTestLoad.xcodeproj\n```\n\n## 基本使用\n\n本项目主要作为学习资料和面试题库，核心使用方式为**阅读源码注解**与**运行调试 Demo**。\n\n### 1. 阅读 Runtime 核心解析\n\n打开项目中的 Markdown 文件或直接在 Xcode 中浏览代码，重点关注以下核心问题的解答：\n\n*   **内存模型**: 查看 `isa`、对象、类、元类（metaclass）的结构定义。\n*   **属性与方法列表**: 对比 `class_copyIvarList` 与 `class_copyPropertyList` 的底层实现差异。\n*   **Category 加载机制**: 阅读 `attachCategories` 函数实现，理解 Category 方法如何“覆盖”原类方法以及加载顺序（倒序插入）。\n\n### 2. 运行调试示例\n\n以验证 Category 加载顺序为例：\n\n1.  在 Xcode 中打开 `demos\u002FTestLoad 方法` 工程。\n2.  找到 `Person` 类及其多个 Category 文件。\n3.  在 `+load` 方法处打断点。\n4.  点击 **Run** (Cmd + R) 运行项目。\n5.  观察控制台输出顺序及断点触发次序，验证“先编译的先调用”还是“后编译的先加载”的结论。\n\n### 3. 关键代码片段示例\n\n在调试过程中，您可以参考以下核心逻辑（源自项目解析）：\n\n```objective-c\n\u002F\u002F 模拟 attachLists 逻辑：新编译的 Category 会被插入到数组头部\nvoid attachLists(List* const * addedLists, uint32_t addedCount) {\n    if (addedCount == 0) return;\n\n    if (hasArray()) {\n        \u002F\u002F many lists -> many lists\n        uint32_t oldCount = array()->count;\n        uint32_t newCount = oldCount + addedCount;\n        setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));\n        array()->count = newCount;\n        \n        \u002F\u002F 将旧内容向后移动，新内容放在起始位置\n        memmove(array()->lists + addedCount, array()->lists, \n                oldCount * sizeof(array()->lists[0]));\n        memcpy(array()->lists, addedLists, \n               addedCount * sizeof(array()->lists[0]));\n    }\n    \u002F\u002F ... 其他情况处理\n}\n```\n\n通过上述步骤，您可以快速建立起对 iOS Runtime 底层机制的直观认识，并为应对大厂技术面试做好充分准备。","某 iOS 资深开发者正在备战字节跳动与阿里巴巴的年终职级晋升面试，急需深入理解 Objective-C Runtime 底层机制以应对高难度的原理性提问。\n\n### 没有 bytedance-alibaba-interview 时\n- **源码查阅零散低效**：面对 `class_copyIvarList` 与 `class_copyPropertyList` 的区别等细节问题，需在 Apple 开源官网海量代码中盲目搜索，难以定位关键逻辑。\n- **核心概念理解模糊**：对于 `class_rw_t` 与 `class_ro_t` 的结构差异及内存布局缺乏直观认知，仅凭碎片化博客文章难以构建完整的知识体系。\n- **动态加载机制困惑**：在 Category 加载顺序、同名方法覆盖规则等高频考点上，因缺乏可运行的调试源码验证，只能死记硬背结论，无法应对面试官的追问。\n- **备考资料良莠不齐**：网络上的面试题解答往往缺少官方源码佐证，导致复习时对技术细节的正确性存疑，增加焦虑感。\n\n### 使用 bytedance-alibaba-interview 后\n- **源码对照一目了然**：直接参考项目中整理好的 `objc4-750` 可运行源码与详细注释，快速厘清属性列表获取的具体实现路径，节省大量检索时间。\n- **结构模型可视化**：借助清晰的 `isa`、元类及 `class_rw_t\u002Fro_t` 内存模型图，瞬间掌握对象存储本质，能够流畅绘制并讲解底层架构。\n- **机制验证有据可依**：通过复现 Category 加载与方法交换的测试案例，从源码层面彻底吃透 `+load` 执行时序与方法覆盖逻辑，面试回答底气十足。\n- **真题解析精准高效**：基于掘金高赞文章整理的权威题解，确保每一个知识点的准确性，将复习重心从“找答案”转移到“消化原理”。\n\nbytedance-alibaba-interview 通过将晦涩的 Runtime 源码转化为结构化的面试通关指南，帮助开发者从“知其然”跨越到“知其所以然”，极大提升了大厂面试的准备效率与通过率。","https:\u002F\u002Foss.gittoolsai.com\u002Fimages\u002Fcolourful987_bytedance-alibaba-interview_c125813f.png","colourful987","PMST","https:\u002F\u002Foss.gittoolsai.com\u002Favatars\u002Fcolourful987_f49ff53f.jpg","HEROS COME AND GO, LEGENDS LIVE FOREVER","Bytedance","Hangzhou","mcxcode@163.com",null,"https:\u002F\u002Fgithub.com\u002Fcolourful987",[86],{"name":87,"color":88,"percentage":89},"Shell","#89e051",100,702,171,"2026-04-03T19:32:16","macOS","未说明",{"notes":96,"python":94,"dependencies":97},"该项目并非 AI 模型工具，而是一套关于 iOS Objective-C Runtime 的面试题解答与源码分析文档。运行环境主要为 macOS 系统下的 Xcode 开发环境，用于编译和调试提供的 Objective-C 测试 Demo（如 TestLoad 方法）。文中提到的依赖主要是 Apple 开源的 objc4 源码（版本 750），用于底层原理分析，无需 Python、GPU 或特定的深度学习框架。",[98,99],"Xcode","Objective-C Runtime (objc4)",[18],"2026-03-27T02:49:30.150509","2026-04-07T22:59:37.111035",[104,109,114],{"id":105,"question_zh":106,"answer_zh":107,"source_url":108},23053,"objc4_781 版本中 class_rw_t 结构发生了什么变化？如何获取方法列表和属性列表？","在 objc4_781 及更高版本中，Apple 隐藏了直接访问方法列表和属性列表的方式，将其封装到了新的 class_rw_ext_t 结构中。维护者已确认该变动，并计划更新相关文档以适配新版本。建议开发者关注官方源码更新或等待项目维护者的详细解析。","https:\u002F\u002Fgithub.com\u002Fcolourful987\u002Fbytedance-alibaba-interview\u002Fissues\u002F4",{"id":110,"question_zh":111,"answer_zh":112,"source_url":113},23054,"Category 的 +load 方法调用顺序是怎样的？","Category 的 +load 方法调用顺序遵循编译顺序：先编译的 Category 先调用，后编译的后调用（即按照编译时间的先后顺序执行）。此前文档中关于“后编译的先调用”的描述有误，维护者已修正该错误并增加了验证 Demo 以供参考。","https:\u002F\u002Fgithub.com\u002Fcolourful987\u002Fbytedance-alibaba-interview\u002Fissues\u002F1",{"id":115,"question_zh":116,"answer_zh":117,"source_url":118},23055,"AutoreleasePool 底层使用的数据结构是什么？","AutoreleasePool 底层使用的是以栈为节点的双向链表结构，而非普通的列表。此前文档中存在笔误，维护者已对该描述进行了修正。","https:\u002F\u002Fgithub.com\u002Fcolourful987\u002Fbytedance-alibaba-interview\u002Fissues\u002F3",[]]