问题集 基础部分
基础题部分
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 会加一个锁来保障线程安全,也就是保证了读写操作是安全的,并且引用计数会 +1,来向调用者保证这个对象会一直存在.
但是不能保证线程安全,比如当线程A setter操作时,这时B线程的setter操作会等待。当A线程的setter结束后,B线程进行setter操作,
然后当A线程需要getter操作时,却有可能获得了在B线程中的值,这就破坏了线程安全
3: 被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<HaveOld, HaveNew>
static void lockTwo(SideTable *lock1, SideTable *lock2);
template<HaveOld, HaveNew>
static void unlockTwo(SideTable *lock1, SideTable *lock2);
};
参考:
objc-weak.mm源码
weak 弱引用的实现方式
iOS 底层解析weak的实现原理
4: 关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
关联对象(AssociatedObject)是Objective-C 2.0运行时的一个特性,允许开发者对已经存在的类在扩展中添加自定义的属性。在实际生产过程中,比较常用的方式是给分类(Category)添加成员变量。
关联对象有什么应用?
关联对象可以在运行时给指定对象绑定一个有生命周期的变量。
1.由于不改变原类的实现,所以可以给原生类或者是打包的库进行扩展,一般配合Category实现完整的功能。
2.ObjC类定义的变量,由于runtime的特性,都会暴露到外部,使用关联对象可以隐藏关键变量,保证安全。
3.可以用于KVO,使用关联对象作为观察者,可以避免观察自身导致循环。
系统如何管理关联对象?
系统通过管理一个全局哈希表,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter传入的参数协议,来管理对象的生命周期。
其被释放的时候需要手动将其指针置空么?
当对象被释放时,如果设置的协议是OBJC_ASSOCIATION_ASSIGN,那么他的关联对象不会减少引用计数,其他的协议都会减少从而释放关联对象。
unsafe_unretain一般认为外部有对象控制,所以对象不用处理,因此不管什么协议,对象释放时都无需手动讲关联对象置空。
5: KVO的底层实现?如何自己动手实现 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: Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么
AutoreleasePool是一个堆栈,里面装着指针。那么栈的底层实现是什么呢?是数组。
AutoreleasePool全名叫NSAutoreleasePool。它就是一个对象引用计数自动处理器,在官方文档中被称为是一个类。
AutoreleasePoolPage的数据结构如下:
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
AutoreleasePool没有单独的数据结构,是由AutoreleasePage组成的双向数据链表,每一个AutoreleasePage对应一个thread,前向节点为parent,后向节点为child,next为page内部对象的指针,指向当前对象。
AutoreleasePoolPage::push()
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
对链表进行查询,将新生成的page赋给当前page的child。
AutoreleasePoolPage::pop()
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
pop(coldPage()->begin());
} else {
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
return;
}
page = pageForPointer(token);
stop = (id *)token;
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
// Start of coldest page may correctly not be POOL_BOUNDARY:
// 1. top-level pool is popped, leaving the cold page in place
// 2. an object is autoreleased with no pool
} else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
AutoreleasePool 浅析
autoreleasepool 源码解析
7: 讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
对象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对象如何进行内存布局
8: class_ro_t 和 class_rw_t 的区别?
class_ro_t与class_rw_t的最大区别在于一个是只读的,一个是可读写的,实质上ro就是readonly的简写,rw是readwrite的简写。
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: 一个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实现原理
11: 为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?
_weak是为了解决循环引用问题,(如果block和对象相互持有就会形成循环引用)
而__strong在Block内部修饰的对象,会保证,在使用这个对象在block内,
这个对象都不会被释放,strongSelf仅仅是个局部变量,存在栈中,会在block执行结束后回收,不会再造成循环引用。
__strong主要是用在多线程中,防止对象被提前释放。
参考:
iOS __weak和__strong在Block中的使用
有时候我们经常也会被问到block为什么 常使用copy关键字?
官方中有如下一段话:
总结别人的话来说:
block 使用
copy
是从 MRC遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。
如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”
12: RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
RunLoop .png字面意思是“消息循环、运行循环”,runloop内部实际上就是一个do-while循环,它在循环监听着各种事件源、消息,对他们进行管理并分发给线程来执行。
线程和 RunLoop 之间是一一对应的。
运行机制从官方文档说明
翻译过来如下:
1.通知观察者将要进入运行循环。
2.通知观察者将要处理计时器。
3.通知观察者任何非基于端口的输入源即将触发。
4.触发任何准备触发的基于非端口的输入源。
5.如果基于端口的输入源准备就绪并等待触发,请立即处理该事件。转到第9步。
6.通知观察者线程即将睡眠。
7.将线程置于睡眠状态,直到发生以下事件之一:
- 事件到达基于端口的输入源。
- 计时器运行。
- 为运行循环设置的超时值到期。
- 运行循环被明确唤醒。
8.通知观察者线程被唤醒。
9.处理待处理事件。
- 如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第2步。
- 如果输入源被触发,则传递事件。
- 如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
10.通知观察者运行循环已退出。
这里借用一下这里的图片
参考
深入理解RunLoop
关于Runloop的原理探究及基本使用
13:谈谈消息转发机制实现。
先会调用
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 消息发送与转发机制原理
深入浅出理解消息的传递和转发机制
14: 哪些场景可以触发离屏渲染?(知道多少说多少)
官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。
shouldRasterize(光栅化)
masks(遮罩)
shadows(阴影)
edge antialiasing(抗锯齿)
group opacity(不透明)
复杂形状设置圆角等
渐变
Text(UILabel, CATextLayer, Core Text, etc)...