Advertisement

最新iOS大厂面试题大全

阅读量:

1. ARC帮我们做了什么?

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

为我们的代码,在适当的位置插入release、retarn和mium指令或者优化这些指令以减少计数器的操作

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

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

该load方法指出,在应用程序启动并执行时(当应用程序启动并执行时),Runtime程序直接获取了该load对象,并立即进行操作(而无需借助objc消息机制或其他途径来进行操作)。

load_images(const char *path __unused, const struct mach_header *mh) {
// 准备分类信息
prepare_load_methods((const headerType *)mh);
// 执行加载操作
call_load_methods();
}

void prepareLoadMethods(const headerType* mhdr) {
classref_t* classList = _getObjc2NonlazyClassList(mhdr, &count);
for (int i = 0; i < count; ++i) {
schedule_class_load(remapClass(classList[i]));
}
category_t** categoryList = _getObjc2NonlazyCategoryList(mhdr, &count);
for (int i = 0; i < count; ++i) {
category_t* cat = categoryList[i];
add_category_to_loadable_list(cat);
}
}

static void schedule_class_load(Class cls) {
// 开始递归,加载superclass
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
}

该函数用于执行特定的操作流程,并包含一个复杂的循环结构来处理数据流的分析与分类任务。在每次迭代过程中,在满足特定条件的情况下(即当前可解析的数据包数目大于零),系统会触发相应的处理逻辑;此外,在完成一次完整的数据包解析后会进入下一轮迭代过程;整个算法设计的目标是在有限的时间内尽可能地提高数据分类的准确性率。

static void call_class_loads(void) {
// 收集所有需要重写的load方法所属的类
struct loadable_class *classes = loadable_classes;
// 这里对detached列表中的每一个类执行相应的load操作
for (int i = 0; i < used; i++) {
Class cls = classes[i].cls;
// 获取到对应load方法的引用
load_method_t load_method = (load_method_t)classes[i].method;
// 执行该load操作以加载指定对象的方法
(*load_method)(cls, SEL_load);
}
return NULL; // 函数执行完毕后返回空指针
}

static bool call_category_loads(void) {
// 在prepare_load_methods 方法里面准备了所有重新load方法的category
struct loadable_category *cats = loadable_categories;
for (int i = 0; i < used; i++) {
// 获取到catgegory
Category cat = cats[i].cat;
// 获取category 的load 方法的IMP实现
load_method_t load_method = (load_method_t)cats[i].method;
cls = _category_getClass(cat);
if (cls && cls->isLoadable()) {
// 调用load方法
(*load_method)(cls, SEL_load);
}
}
}
void _class_initialize(Class cls) {
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
// 又是个递归
_class_initialize(supercls);
}
// 调用 initialize方法
callInitialize(cls);
}
// objc_msgSend 调用 initialize 方法
void callInitialize(Class cls) {
// **注意:因为使用了objc_msgSend,有可能调用class的 initialize **
objc_msgSend(cls, SEL_initialize);
}
总结:
load方法一个类只会调用一次(除去手动调用),而调用的数序是,从superclass -> class -> category,category里面的顺序是先编译,先调用
initialize方法,一个类可能会调用多次,如果子类没有实现initialize方法,当第一次使用此类的时候,会调用superclass。而调用的顺序是,superclass -> 实现initialize的category 或者 实现了initialize方法(没有category实现initialize) 或者 superclass的initialize (没有子类和category实现initialize方法)

在调用初始化方法时的行为与其它方法具有相似性,在程序中通常会采用消息机制发送objc_msgSend指令以完成此类操作。在具体的执行流程中遵循以下顺序:如果没有superclass提供初始化功能,则该类将继承自其父类或继承自实现了初始化功能的相关类;如果该类未定义自己的初始化方法,则会默认继承父类( superclass)中的 initialize 方法;这是因为 initialize 的底层逻辑依赖于objc_msgSend 消息机制。

看下Runtime底层调用_class_initialize的源码

该load方法在类加载时按照前后次序被执行,并且其内部按类别层次结构中从父到子再到子类别依次执行。但需要注意的是,在这种情况下该操作通常会触发两次(自动触发和手动触发的情况),但每次该load操作仅执行一次(手动触发的情况除外)。

一下为Runtime源码的主要代码

3.category属性是存储在那里?

我们都知道,在 category 类中我们可以采用 Runtime 的 objc_setAssociatedObject 和 objc_getAssociatedObject 方法来实现 get 和 set 功能。具体存储位置是什么呢?

其实此属性的值保存在一个AssociationsManager里面。

我们也是可以根据源码看一下

setAssociativeReference(id object, void key, id value, uintptr_t policy) {
/
简化代码 /
id new_value = value ? acquireValue(value, policy) : NULL;
{
AssociationsManager manager;
const auto& associations = associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
ObjectAssociationMap
refs = new ObjectAssociationMap();
associations[disguised_object] = refs;
(*refs)[key] = OcObjectAssociation(policy, new_value);
}
}
}

4.category方法是如何添加的?

当我们在类别中新增相同功能时,在这种情况下程序会自动使用category内部提供的方法来执行任务;而不会调用我们的class内部方法。

在编译过程中, 该编译器将其转换为一个名为category_t的特殊数据类型. 在类初始化阶段, 程序会将这些分类信息同步到名为class_rw_t的对象中, 其中包含method(方法)、property(属性)以及protocol(协议)等多种类型. 在同步过程中,默认的做法是将这些来自category的信息附加到class对象之前定义的内容中. 而在后续的方法调用过程中, 程序会遍历class_rw_t对象中的所有方法, 因此在遇到特定标识IMP时就会返回.

使用memmove,将类方法移动到后面

使用memcpy,将分类的方法copy到前面

当多个分类有相同的方法的时候,调用的顺序是后编译先调用

在类初始化同步category的过程中,在线同步时会采用i从当前值开始递减的循环结构(即while(i–)),以实现将新增编译顺序调整为在列表头部的效果。

5. OC 的消息机制

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

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

第一步将执行-(id)forwardingTargetForSelector:(SEL)aSelector方法,在此处将会返回一个响应aSelector的对象。当返回值不为nil时系统将不再中断消息转发过程而会继续向下查找对应的IMP对象并完成整个数据转发流程

在第二步中进行处理时,请注意以下情况:当第一步方法调用返回nil或self(表示系统内部操作),则系统将转而进行下一步操作,并应为此为该aNSED定义相应的aNSED签名。

第三步, 如果返回了一个签名, 将被传递到 -(void)forwardInvocation:(NSInvocation *)anInvocation,随后我们可以从 anInvocation 中获取参数, target 以及方法名称等信息, 这将提供更多操作的可能性就变得很多,具体情况而定。此时无需进行任何操作也不会出现问题。

注意:在处理类方法时, 也就是说, 在这种情况下我们将以上方法替换为+号就能完成相应的转发操作.

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

当且仅当该方法属于类方法实例时,则会调用+(BOOL)resolveClassMethod:(SEL)sel;而如果该方法属于实例方法,则会调用+(BOOL)resolveInstanceMethod:(SEL)sel

我们有能力完成这两个方案;通过Runtime类中的class_addMethod方法来添加相应的IMP

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

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

在调用方法时(每当某个方法被调用),该操作都会被转换为objc_msgSend来进行数据传递

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

第二步将从该对象的catch属性中查找相关数据。该属性是一个散列表结构,并基于@selector指定的方法名来获取相应的IMP实例,并启动调用流程。

下一步,在上一步未能找到结果的情况下,则会继而继续查找该类对象内部的class_rw_t字段中的methods属性(即方法列表),并进行遍历操作以确定相应的方法所属的IMP类别;若发现相关 IMP 存在,则将其信息记录于 catch 表中。

第4步,在第3步未找到的情况下,则会根据类对象中的 superclass 指针进行查找;如果找不到 superviewsight 的 catch 环节,则会继续在 superclass 内的 class_rw_t 中查找 methods(方法列表),以便全面遍历并最终确定该方法所属的 IMP 并将其记录在 catch 表中

在第5步中,在第4步未成功查找后(即在第4步未成功查找后),将根据类的superclass执行第4步操作。

…………

第六步。如果持续查找父类均未找到应答的方法,则将被动态解析处理

消息传递

动态解析

消息转发

6.weak表是如何存储__weak指针的

  • 该关键词weak具有明确的行为特征:即在对象被销毁时会将其引用设为nil。同时,在其底层实现中也采用类似的方式组织存储:以键值对的形式将引用与对应的对象进行关联存储于哈希表中。 *

当对象的引用强度被指定为__weak时,在内存管理中会触发特定机制以释放不再需要的引用资源。具体而言,在这种情况下,默认情况下会调用objc_storeWeak函数,并接受并处理两个参数:一个为目标地址(id *location),另一个为新对象的引用地址(id newObj)。

第一个参数为指针,第二个参数为所指向的对象

第二步,继续调用storeWeak(location, (objc_object *)newObj)

第一个参数是指针,第二个参数是对象的地址

再次方法里面会根据对象地址生成一个SideTables对象

在本步骤中,请执行将id weak_register_no_lock(...)注册到内存中的操作

weak_table即为SideTables的一个属性, referent_id对应于对象, referrer_id而是一个指向弱引用的指针

在此里面会根据对象地址和指针生成一个weak_entry_t

第四步中将执行static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)

重点在于,在该方法中会根据对象&weak_table->mask(其中 weak 表内部可容纳数量减一)进行处理。具体而言,在实际应用中若表格最多容纳 10 个对象,则 mask 值为 9。系统将根据当前状态生成相应的索引值,并在以下情况下采取递增索引的方式继续搜索下一个可用位置:当当前索引值已占用时,则采用递增索引的方式继续搜索下一个可用位置,并将 new_entry 存入指定位置以完成数据保存

注意:当一个对象被多个weak引用指针引用时,在内存中也会生成一个新的entry,并将其referrers属性存储这些引用来源。

以下为简易的源码:

id
objc_storeWeak(id *location, id newObj)
{
return storeWeak<DoHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object *)newObj);
}
static id
storeWeak(id *location, objc_object *newObj) {
// 根据对象生成新的SideTable
SideTable *newTable = &SideTables()[newObj];
newObj = (objc_object *)
weak_register_no_lock(&newTable->weak_table, (id)newObj, location, crashIfDeallocating);
}
id
weak_register_no_lock(weak_table_t *weak_table, id referent_id,
id *referrer_id, bool crashIfDeallocating){
objc_object *referent = (objc_object *)referent_id;
objc_object **referrer = (objc_object **)referrer_id;

// 根据对象和指针生成一个entry
weak_entry_t new_entry(referent, referrer);
// 检查是是否该去扩容
weak_grow_maybe(weak_table);
// 将新的entry 插入到表里面
weak_entry_insert(weak_table, &new_entry);
}
static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
weak_entry_t *weak_entries = weak_table->weak_entries;

复制代码
    size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
    index = (index+1) & weak_table->mask;
      if (index == begin) bad_weak_table(weak_entries);
       hash_displacement++;
    }
    weak_entries[index] = *new_entry;
    weak_table->num_entries++;
    
    
      
      
      
      
      
      
      
      
      
      
    
    AI写代码

当 weak_table 的存储数量达到最大容量的四分之三时,在进行扩增操作前,系统会以两倍的速度扩展当前的数据结构,并在扩增之后重新生成索引。完成保存操作。

  • 以下为简易的源码:

static void maybe_robust增长(HashMap弱表弱表参数){
size_t初始大小=(弱表参数是否存在掩码字段?存在则取掩码值加一否则取零);
如果弱表中的条目数量大于等于初始大小乘以四分之三{
调用弱表重新配置函数将新尺寸传递给参数;
}
}
static void重新配置HashMap弱表(size_t新尺寸HashMap弱表*){
size_t当前大小=获取当前哈希表大小;
HashMap弱表参数*旧条目指针=当前哈希表条目;
//初始化动态内存空间
new条目指针=(动态内存分配新尺寸块);
//设置新掩码值为新尺寸减一;
设置当前哈希表掩码字段为新尺寸减一;
遍历旧条目指针到末尾位置{
如果条目有引用目标{
调用插入新条目的函数将旧条目添加回哈希表;
}
}
}

7. 方法catch表是如何存储方法的

我们了解,在执行某个方法时,系统会通过检查对象的isa属性来确定其所属的对象类别,并随后转而进行在catch表中的相关查询操作。

实际上catch是一个基于@selector的方法表。通过@selector(方法名)获取索引值,并结合catch表的最大容量减一来确定具体的存储位置。如果当前catch表中已存在对应的方法,则会将当前索引值加一以寻找下一个可用位置。若当前计算出的索引位置对应的存储单元为空(即nil)时,则会将该响应的方法加入到catch表中去。

核心代码

static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) {
// 定位类对象的catch地址
cache = &cls->cache; // 确定key cache_key_t key = (cache_key_t) sel;
// 确定对应的桶
$bucket = cache->find(key, receiver);
}

bucket_t* cache_t::get(cache_key_t k, id receiver) {
// Catch表中的记录条目
bucket_t* b = buckets();
// Catch表表示的掩码最大值减一
mask_t m = mask();

复制代码
    mask_t begin = cache_hash(k, m);
    mask_t i = begin;
    do {
    if (b[i].key() == 0  ||  b[i].key() == k) {
        return &b[i];
    }
    } while ((i = cache_next(i, m)) != begin);
    
    
      
      
      
      
      
      
      
    
    AI写代码

需要注意的是cache_next函数会返回下一个掩码值。
该函数采用位运算实现高效计算。
具体来说,
输入参数i表示当前掩码,
mask参数用于限定结果范围。
每次调用时,
将i加一并按mask进行位与操作,
得到下一个掩码值。
同时,
catch表和其他结构如weak_table一样
遵循与weak_table相同的2倍扩增策略。
需要注意的是,
当catch表进行扩容时,
以前缓存的方法会丢失之前缓存的数据。

简易代码

void expand(cache_t& cache_t) {
uint32_t oldCapacity = this->capacity();
uint32_t newCapacity;
if (old_capacity != 0) {
new_capacity = old_capacity * 2;
} else {
new_capacity = INIT_CACHE_SIZE;
}
cache.resize(old_capacity, new_capacity);
}

void cache_t::relocate(mask_t oldCapacity, mask_t newCapacity) // 获取旧缓存桶
{
// 获取旧缓存桶
auto oldBuckets = buckets();
// 分配新缓存桶
auto newBuffers = allocateNewBuffers(newCapacity);
// 释放已使用的缓存桶
cache_collect_free(oldBuckets, oldCapacity);
}

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

在最新版本中的Objective-C对象中,isa指针不再是单纯指向所属类地址的指针;现在它成为了一个共享体,并利用位域存储了更多的信息

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

启动顺序

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

通过调用 UIApplicationMan函数(即AppDelegate的应用:application:didFinishLaunchingWithOptions:),我们可以实现应用程序函数的获取

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

通过调用函数call_load_methods,在内部实现模块load_images的功能

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

尚未完全掌握的所有相关符号(包括Class、Protocol、Selector以及IMP…)均以规范的形式被加载至内存中,并由runtime系统进行管理。

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

当Dyld完成对可执行文件和动态库的装载后, 将会向Runtime发出通知以启动后续处理流程.

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

Runtime

main函数调用

App启动速度优化

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

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

在不考虑用户体验的情况下,在某些情况下延迟部分操作流程,并避免所有操作立即触发FinishLaunching

此外,在应用的根级UIViewController中所实现的viewDidload功能中,请避免长时间的操作

一些网络请求

一些第三方的注册

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

优化动态库管理,并整合自定义动态库资源,在系统维护过程中建立高效的维护机制以定期清理不再需要的资源

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

Swift尽量使用struct

dyld

Runtime

main

提示:通过配置环境变量可实现输出App启动时间分析结果 (Edit scheme -> Run -> Arguments)

10.App瘦身
资源(图片、音频、视频等)

可以采取无损压缩

使用LSUnusedResources去除没有用的资源 LSUnusedResources

可执行文件瘦身

移除相关联的产品、将字符串设置为只读模式以及将符号默认隐藏的状态设为true

移除部分C++和Objective-C异常支持(C++和Objective-C异常启用设为false)

通过AppCode工具检测未曾被调用的代码,在主菜单中选择"菜单栏"->"Code"->"Inspect Code"路径,在编译完成后能够识别并列出尚未被调用的类。

生成LinkMap文件,可以查看可执行文件的具体组成

可借助第三方工具解析LinkMap文件LinkMap

Link Map解析结果

推荐:

2020年大厂必备面试题

进群直接领取面试文件

作为一名开发者,在这个领域养成良好的学习氛围以及参与社群圈尤其关键。这是我创建的一个iOS交流群:651612063 入群口令是111。无论你是新手还是资深开发者加入我们都不会有问题。我们可以分享BAT公司的面试题以及阿里的面试经验,并探讨技术问题。共同进步吧!一起探讨技术问题吧!

全部评论 (0)

还没有任何评论哟~