几个iOS基础题目总结

前言:
最近看到大佬汇集的iOS面试题,个人感觉还不错,打算试着探索一下这些问题的答案,也巩固一下我自己基础知识。这篇文章先总结一下基础知识的答案吧。其中有些错误或不全的地方望指教。

-----------------------------------------持续更新中---------------------------------

iOS 基础题

1:分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

分类和扩展的作用

1:category的主要作用是为已经存在的类添加方法
下面也有其他作用可以了解下:
2:可以把类的实现分开在几个不同的文件里面,
(可以减少单个文件的体积
可以把不同的功能组织到不同的category里
可以由多个开发者共同完成一个类
可以按需加载想要的category)
3:模拟多继承
4:把framework的私有方法公开
扩展的作用:为一个类添加额外的原来没有变量,方法和属性

类别与类扩展的区别

1:extension在编译期决定,它就是类的一部分,
在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,
它伴随类的产生而产生,亦随之一起消亡。
extension一般用来隐藏类的私有信息,
你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension
但是category则完全不一样,它是在运行时候决定的.
类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
extension可以添加实例变量,而category是无法添加实例变量的
2:类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。

分类局限性

(1)无法向类中添加新的实例变量。
(2)名称冲突,即当类别中的方法与原始类方法名称冲突时,类别具有更高的优先级。
(3)如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法

在runtime层,category用结构体category_t

typedef struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;

从源码中我们可以看出分类结构体成员:

1)类的名字(name)
2)类(cls)
3)category中所有给类添加的实例方法的列表(instanceMethods)
4)category中所有添加的类方法的列表(classMethods)
5)category实现的所有协议的列表(protocols)
6)category中添加的所有属性(instanceProperties)

参考链接:
分类和扩展说明参考
美团关于分类的源码解析说明
官方分类源码地址

2:atomic的实现机制;为什么不能保证绝对的线程安全

这个问题我觉得看这个就够了stackoverflow关于atomic和nonatomic的一个问题
当然也可以看别人根据stackoverflow这个问题总结好的中文说明

简单来说:atomic 会加一个锁来保障线程安全,也就是保证了读写操作是安全的,并且引用计数会 +1,来向调用者保证这个对象会一直存在.
但是不能保证线程安全,比如当线程A setter操作时,这时B线程的setter操作会等待。当A线程的setter结束后,B线程进行setter操作,
然后当A线程需要getter操作时,却有可能获得了在B线程中的值,这就破坏了线程安全

3:哪些场景可以触发离屏渲染?

首先我们要知道什么是离屏渲染:
离屏渲染Off-Screen Rendering 指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染会先在屏幕外创建新缓冲区,离屏渲染结束后,再从离屏切到当前屏幕
还有另外一种屏幕渲染方式-当前屏幕渲染On-Screen Rendering ,
指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
以下方式会触发离屏幕渲染
1:使用系统提供的圆角效果也会触发离屏渲染.(masksToBounds = true&&cornerRadius>0才会引发离屏渲染)
2:重写drawRect
3:layer.shadow(Shawdow 可以通过指定路径来取消离屏渲染)
4:layer.mask(Mask 效果无法取消离屏渲染,使用混合图层的方法来模拟 mask 效果,性能各方面都是和无效果持平。)
5:layer.allowsGroupOpacity(GroupOpacity 是指 CALayer 的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性,
开启离屏渲染的条件是:layer.opacity != 1.0并且有子 layer 或者背景图。

layer.allowsEdgeAntialiasing(该属性用于消除锯齿,离屏渲染条件旋转视图并且设置layer.allowsEdgeAntialiasing = true)
6:layer.shouldRasterize(光栅化会触发离屏渲染,开启 Rasterization=true 后,GPU 只合成一次内容,然后复用合成的结果;合成的内容超过 100ms 没有使用会从缓存里移除,在更新内容时还会产生更多的离屏渲染。对于内容不发生变化的视图,原本拖后腿的离屏渲染就成为了助力)

参考:
离屏渲染优化详解
Instruments性能优化-Core Animation
绘制像素到屏幕上
界面流畅性优化

4:被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录.
objc_clear_deallocating该函数的动作如下:
1、从weak表中获取废弃对象的地址为键值的记录
2、将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil
3、将weak表中该记录删除
4、从引用计数表中删除废弃对象的地址为键值的记录
SideTable 这个结构体主要用于管理对象的引用计数和 weak 表。在 NSObject.mm 中声明其数据结构:

struct SideTable {
    spinlock_t slock;//保证原子操作的自旋锁
    RefcountMap refcnts;//引用计数的 hash 表
    weak_table_t weak_table;//weak 引用全局 hash 表

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

参考:
objc-weak.mm源码
weak 弱引用的实现方式
iOS 底层解析weak的实现原理

5:KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,
对象就神奇的变成了新创建的子类的实例

关闭默认的KVO重写方法

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    return NO;
}//如果返回NO,KVO无法自动运作,需手动触发

键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey
在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后,
observeValueForKey:ofObject:change:context: 会被调用,
并且 didChangeValueForKey: 也会被调用。如果可以手动实现这些调用,就可以实现手动触发.

参考:
如何自己动手实现 KVO
apple用什么方式实现对一个对象的KVO

6:一个int变量被__block修饰与否的区别?

Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。
__block 所起到的作用就是只要观察到该变量被 block 所持有。
__block 后,实际上成为了一个结构体,block内截获了 该结构体的指针。
在block中使用自动变量时,使用的是 指针指向的结构体中的 自动变量。
ARC环境下,会被copy到堆上。(ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。
ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上。)
MRC环境下,只有copy,__block才会被复制到堆上,否则,__block一直都在栈上。

测试,其实最好的方法是动手测试,这边我只测试了ARC环境下的。我在.main.m的测试代码如下:

  __block int a1 = 1;
        int a2 = 1;
        NSLog(@"__block定义前a1:%p", &a1);
        NSLog(@"__block定义前a2:%p", &a2);;
        void (^foo)(void) = ^{
            a1 = 2;
        
            NSLog(@"block内部a1:%p", &a1);
            NSLog(@"block内部a2:%p", &a2);
        };
        NSLog(@"重新定义后a1:%p", &a1);
        NSLog(@"重新定义后a2:%p", &a2);
        NSLog(@"foo =%@",foo);
        foo();
    ——---------------------- 输出结果如下:-------------------------------    
 
        __block定义前a1:0x7fff53814128 
        __block定义前a2:0x7fff5381410c  
        重新定义后a1:0x60400003dd98
        重新定义后a2:0x7fff5381410c
        foo =<__NSMallocBlock__: 0x60c000244830>
        block内部a1:0x60400003dd98
        block内部a2:0x60400025dbd8

通知打印结果可以发现a1,a2blcok内部和定义前的地址字节数相差很大,堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,所以a1和a2在block内部都会被copy到堆上,只不过一个值的copy,一个是地址copy。

然后clang -rewrite-objc main.m查看一下源码,如果clang -rewrite-objc报错,可以像我一样尝试
xcrun -sdk iphonesimulator11.0 clang -rewrite-objc main.m

源码如下:

//加上__block 后,实际上成为了一个结构体,block内截获了 该结构体的指针
struct __Block_byref_a1_0 {
  void *__isa;
__Block_byref_a1_0 *__forwarding;
 int __flags;
 int __size;
 int a1;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a2;
  ////截获的结构体指针
  __Block_byref_a1_0 *a1; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a2, __Block_byref_a1_0 *_a1, int flags=0) : a2(_a2), a1(_a1->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  //指针引用
  __Block_byref_a1_0 *a1 = __cself->a1; // bound by ref
  //a2只是单纯的值拷贝,。Block仅仅捕获了a2的值,并没有捕获a2的内存地址。
  int a2 = __cself->a2; // bound by copy

            (a1->__forwarding->a1) = 2;

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_2, &(a1->__forwarding->a1));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_dd_4kldckw11bv3zn6tgktzys440000gn_T_main_5a4382_mi_3, &a2);
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a1, (void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a1, 8/*BLOCK_FIELD_IS_BYREF*/);}

从源码中可以看出:

带有 __block的变量也被转化成了一个结构体__Block_byref_i_0,很清楚看到了__block的引用过程。
而Block仅仅捕获了a2的值,并没有捕获a2的内存地址。所以在__main_block_func_0这个函数中即使我们重写这个自动变量a2的值,
也无法改变Block外面自动变量a2的值

参考:
iOS中__block 关键字的底层实现原理
深入研究Block捕获外部变量和__block实现原理

7:为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰

_weak是为了解决循环引用问题,(如果block和对象相互持有就会形成循环引用)
而__strong在Block内部修饰的对象,会保证,在使用这个对象在block内,
这个对象都不会被释放,strongSelf仅仅是个局部变量,存在栈中,会在block执行结束后回收,不会再造成循环引用。
__strong主要是用在多线程中,防止对象被提前释放。

参考:
iOS __weak和__strong在Block中的使用

题外话:

有时候我们经常也会被问到block为什么 常使用copy关键字?

官方中有如下一段话:

几个iOS基础题目总结_第1张图片
block 应该用copy

总结别人的话来说:

block 使用 copy 是从 MRC遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。
如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”

8:讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里.

对象isa指向类对象,类对象的isa指向元类。元类isa指向根元类。
根元类的isa指针指向自己,superclass指针指向NSObject类
实例对象结构体只有一个isa变量,指向实例对象所属的类。
类对象有isa,superclass,方法,属性,协议列表,以及成员变量的
描述。
所有的对象调用方法都是一样的,没有必要存在对象中,对象可以有
无数个,类对象就有一个所以只需存放在类对象中

可以从官方objc.h源码里面找到实例定义


/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

可以在runtime.h里面找到类对象的定义

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    //向该类所继承的父类对象 
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    //成员变量列表   
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    //方法列表
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;//方法列表
    //用于缓存调用过的方法    
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    //协议链表用来存储声明遵守的正式协议
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

}

参考:
iOS开发·runtime原理与实践: 基本知识篇
一个objc对象如何进行内存布局

9:iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?

题外话:原谅我看了这道面试题,第一次听说内省,才疏学浅,太菜了,只能好好搜索学习了一番。

内省是对象揭示自己作为一个运行时对象的详细信息的一种能力。包括对象在继承树上的位置,对象是否遵循特定的协议,以及是否可以响应特定的消息。NSObject协议和类定义了很多内省方法,用于查询运行时信息,以便根据对象的特征进行识别。
isKindOfClass:Class
检查对象是否是那个类或者其继承类实例化的对象
isMemberOfClass:Class
检查对象是否是那个类但不包括继承类而实例化的对象
respondToSelector:selector
检查对象是否包含这个方法
conformsToProtocol:protocol
检查对象是否符合协议,是否实现了协议中所有的必选方法。
object_getClass(obj)返回的是obj中的isa指针;
而[obj class]则分两种情况:
一:当obj为实例对象时,
[obj class]中class是实例方法:- (Class)class,
返回的obj对象中的isa指针,返回的是类对象;
二:当obj为类对象(包括元类和根类以及根元类)时,调用的是类方法:+ (Class)class,返回的结果为其本身

可以在ViewController通过简单代码验证一下

//currentClass现在是类对象   
Class currentClass = [self class];
     //都指向实例对象isa指定的类对象
     NSLog(@"currentClass = %p getClass=%p",currentClass ,object_getClass(self));
     //class指向类对象本身  getClass指向类对象isa指向元类
     NSLog(@"currentClass = %p  getClass=%p",[currentClass class],object_getClass(currentClass));
      const char *getClassName = object_getClassName(currentClass);
     //实例对象指向类,类执行元类,元类指向根元类,根元类指向自己
     for (int i = 1; i < 5; i++) {
         NSLog(@"Following the isa pointer %d times gives %p %@---%s", i, currentClass,currentClass,getClassName);
         currentClass = object_getClass(currentClass);
         getClassName = object_getClassName(currentClass);
     }

输出结果如下:

currentClass = 0x10ab29198 getClass=0x10ab29198
currentClass = 0x10ab29198  getClass=0x10ab291c0
Following the isa pointer 1 times gives 0x10ab29198 ViewController---ViewController
Following the isa pointer 2 times gives 0x10ab291c0 ViewController---NSObject
Following the isa pointer 3 times gives 0x10b819e58 NSObject---NSObject
Following the isa pointer 4 times gives 0x10b819e58 NSObject---NSObject

参考
Objective-C的内省(Introspection)小结

10:RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)

这一块平时用的比较少,了解不是很多。其有时间真的好好静下心来看一下相关东西了。

字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。
线程和 RunLoop 之间是一一对应的。
运行机制从官方文档说明
翻译过来如下:
1.通知观察者将要进入运行循环。
2.通知观察者将要处理计时器。
3.通知观察者任何非基于端口的输入源即将触发。
4.触发任何准备触发的基于非端口的输入源。
5.如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。
6.通知观察者线程即将睡眠。
7.将线程置于睡眠状态,直到发生以下事件之一:

  • 事件到达基于端口的输入源。
  • 计时器运行。
  • 为运行循环设置的超时值到期。
  • 运行循环被明确唤醒。

8.通知观察者线程被唤醒。
9.处理待处理事件。

  • 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
  • 如果输入源被触发,则传递事件。
  • 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。

10.通知观察者运行循环已退出。
这里借用一下这里的图片

几个iOS基础题目总结_第2张图片
RunLoop_1.png

参考

深入理解RunLoop
关于Runloop的原理探究及基本使用

11:谈谈消息转发机制实现

先会调用objc_msgSend方法,首先在Class中的缓存查找IMP,没有缓存则初始化缓存。如果没有找到,则向父类的Class查找。如果一直查找到根类仍旧没有实现,则执行消息转发。
1、调用resolveInstanceMethod:方法。允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES,重新开始objc_msgSend流程。这次对象会响应这个选择器,一般是因为它已经调用过了class_addMethod。如果仍没有实现,继续下面的动作。
2、调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil,继续下面的动作。注意这里不要返回self,否则会形成死循环。
3、调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil;传给一个NSInvocation并传给forwardInvocation:
4、调用forwardInvocation:方法,将第三步获取到的方法签名包装成Invocation传入,如何处理就在这里面了,并返回非nil。
5、调用doesNotRecognizeSelector:,默认的实现是抛出异常。如果第三步没能获得一个方法签名,执行该步骤 。

参考:
Objective-C 消息发送与转发机制原理
深入浅出理解消息的传递和转发机制
-----------------------------------------未完待续-----------------------------------

你可能感兴趣的:(几个iOS基础题目总结)