Tips:如果打开后只能看到
GCD有哪些方法api
后续内容无法加载,可以Clone本地查阅!
题目作者:Monkery&&juejin.im/post/5e397ccaf265da570b3f1b02
笔者最近收集梳理了一些iOS相关的问题,其中大部分都是大厂面试或者面试其他人用到的,能命中大部分的面试和日常工作,更希望你可以用它来检验自己
- RunTime相关问题
- 结构模型
- 介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)
- 为什么要设计metaclass
- class_copyIvarList & class_copyPropertyList区别
- class_rw_t 和 class_ro_t 的区别
- category如何被加载的,两个category的load方法的加载顺序,两个category的同名方法的加载顺序
- initialize && Load
- category & extension区别,能给NSObject添加Extension吗,结果如何
- 在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么
- IMP、SEL、Method的区别和使用场景
- load、initialize方法的区别什么?在继承关系中他们有什么区别
- 说说消息转发机制的优劣
- 内存管理
- weak的实现原理?SideTable的结构是什么样的
- 关联对象的应用?系统如何实现关联对象的
- 关联对象的如何进行内存管理的?关联对象如何实现weak属性
- Autoreleasepool的原理?所使用的的数据结构是什么
- ARC的实现原理?ARC下对retain & release做了哪些优化
- ARC下哪些情况会造成内存泄漏
- 其他
- NSNotification相关
- runloop
- KVO
- Block
- 多线程
- 视图&图像相关
- 性能优化
- 开发证书
- 架构设计
- 架构设计
- 其他问题
- 系统基础知识
- 数据结构与算法
runtime是iOS开发最核心的知识了,如果下面的问题都解决了,那么对runtime的理解已经很深了。 runtime已经开源了,官网objc4
参考内容
-
对象:OC中的对象指向的是一个objc_object指针类型,typedef struct objc_object *id;从它的结构体中可以看出,它包括一个isa指针,指向的是这个对象的类对象,一个对象实例就是通过这个isa找到它自己的Class,而这个Class中存储的就是这个实例的方法列表、属性列表、成员变量列表等相关信息的。
-
类:在OC中的类是用Class来表示的,实际上它指向的是一个objc_class的指针类型,typedef struct objc_class *Class
-
OC的Class类型包括如下数据(即:元数据metadata):super_class(父类类对象);name(类对象的名称);version、info(版本和相关信息);instance_size(实例内存大小);ivars(实例变量列表);methodLists(方法列表);cache(缓存);protocols(实现的协议列表);
-
当然也包括一个isa指针,这说明Class也是一个对象类型,所以我们称之为类对象,这里的isa指向的是元类对象(metaclass),元类中保存了创建类对象(Class)的类方法的全部信息。
-
OC对象、类、元类之间的关系
- 类对象、元类对象能够复用消息发送流程机制;
- 单一职责原则
参考内容
property 正常使用会生成对应的实例变量,所以
Ivar
可以查到。
-
class_copyIvarList 获取类对象中的所有实例变量信息,从
class_ro_t
中获取: -
class_copyPropertyList 获取类对象中的属性信息, class_rw_t 的 properties,先后输出了 category / extension/ baseClass 的属性,而且仅输出当前的类的属性信息,而不会向上去找 superClass 中定义的属性。
-
可参考如下代码
Ivar *
class_copyIvarList(Class cls, unsigned int *outCount)
{
const ivar_list_t *ivars;
Ivar *result = nil;
unsigned int count = 0;
if (!cls) {
if (outCount) *outCount = 0;
return nil;
}
mutex_locker_t lock(runtimeLock);
assert(cls->isRealized());
if ((ivars = cls->data()->ro->ivars) && ivars->count) {
result = (Ivar *)malloc((ivars->count+1) * sizeof(Ivar));
for (auto& ivar : *ivars) {
if (!ivar.offset) continue; // anonymous bitfield
result[count++] = &ivar;
}
result[count] = nil;
}
if (outCount) *outCount = count;
return result;
}
objc_property_t *
class_copyPropertyList(Class cls, unsigned int *outCount)
{
if (!cls) {
if (outCount) *outCount = 0;
return nil;
}
mutex_locker_t lock(runtimeLock);
checkIsKnownClass(cls);
assert(cls->isRealized());
auto rw = cls->data();
property_t **result = nil;
unsigned int count = rw->properties.count();
if (count > 0) {
result = (property_t **)malloc((count + 1) * sizeof(property_t *));
count = 0;
for (auto& prop : rw->properties) {
result[count++] = ∝
}
result[count] = nil;
}
if (outCount) *outCount = count;
return (objc_property_t *)result;
}
-
class_rw_t
- class_ro_t
- Protocols
- MethodLists
- Properties
-
class_rw_t 中的 properties 属性按顺序包含分类/扩展/基类中的属性。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
#if SUPPORT_INDEXED_ISA
uint32_t index;
#endif
}
参考内容
- +load 方法是 images 加载的时候调用,假设有一个 XXXClass 类,其主类和所有分类的
+load
都会被调用,优先级是先调用主类,且如果主类有继承链,那么加载顺序还必须是基类的+load
,接着是父类,最后是子类;category 的+load
则是按照编译顺序来的,先编译的先调用,后编译的后调用,可在 Xcode 的 BuildPhase 中查看- 分类添加到了
rw = cls->data()
中的methods/properties/protocols
中,实际上并无覆盖
,只是查找到就返回了,导致本类函数无法加载。
- 分类添加到了
参考内容
-
类第一次被使用到的时候会被调用,底层实现有个逻辑先判断父类是否被初始化过,没有则先调用父类,然后在调用当前类的 initialize 方法.
- 一个类 A 存在多个 category ,且 category中各自实现了 initialize 方法,这时候走的是 消息发送流程,也就说 initialize 方法只会调用一次,也就是最后编译的那个category中的 initialize 方法。
-
如果+load 方法中调用了其他类:比如 B 的某个方法,其实就是走消息发送流程,由于 B 没有初始化过,则会调用其 initialize 方法,但此刻 B 的 +load 方法可能还没有被系统调用过。
- 不可以为系统类添加扩展
参考内容
- OC中的方法调用,编译后的代码最终都会转成
objc_msgSend(id , SEL, ...)
方法进行调用, - 这个方法第一个参数是一个消息接收者对象,runtime通过这个对象的isa指针找到这个对象的类对象,从类对象中的cache中查找(哈希查找,bucket 桶实现)是否存在SEL对应的IMP,若不存在,则会在 method_list中查找(二分查找或者顺序查找),如果还是没找到,则会到supper_class中查找,仍然没找到的话,就会调用_objc_msgForward(id, SEL, ...)进行消息转发。
参考内容
typedef struct method_t *Method;
using MethodListIMP = IMP;
struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
};
参考内容
程序启动 -> load(自动调用) -> [XXAudioTool loadData] ->initialize(自动调用) ->loadData
- load 方法调用时机,而且只调用当前类本身,不会调用superClass 的
+load
方法:- 和load不同,即使子类不实现initialize方法,会把父类的实现继承过来调用一遍。注意的是在此之前,父类的方法已经被执行过一次了,同样不需要super调用。
callInitialize(cls)
然后又调用了lockAndFinishInitializing(cls, supercls)
。
参考内容
-
消息转发
- 消息转发机制(可以间接实现多重继承)
- methodSignatureForSelector:的处理使得NSNull对象可以接受任何 selector 而不产生doesNotRecognizeSelector:异常
- forwardInvocation:的处理使得NSNull实例接受到 unknown selector 时,不做任何处理,即空操作
-
动态方法解析
SideTable参考内容
- SideTable 结构体在 runtime 底层用于引用计数和弱引用关联表,其数据结构是这样:
- 指向某个对象A的所有 weak 关键字修饰的引用都 append 到
weak_entry_t
结构体中的referrers
, 同时weak_entry_t
的referent
就是对象A,之后在dealloc 释放时遍历weak_table
遍历时会判断 referent 是否为对象 A 取到weak_entry_t
,加入到该 SideTable 中weak_table
中:
- 指向某个对象A的所有 weak 关键字修饰的引用都 append 到
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}
struct weak_table_t {
// 保存了所有指向指定对象的 weak 指针
weak_entry_t *weak_entries;
// 存储空间
size_t num_entries;
// 参与判断引用计数辅助量
uintptr_t mask;
// hash key 最大偏移值
uintptr_t max_hash_displacement;
};
id objc_initWeakOrNil(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}
return storeWeak<DontHaveOld, DoHaveNew, DontCrashIfDeallocating>
(location, (objc_object*)newObj);
}
SideTable参考内容
- 置空关联对象:赋值给与nil即可
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
AssociationsManager manager;
// manager.associations() `AssociationsHashMap` 对象(*_map)
AssociationsHashMap &associations(manager.associations());
// intptr_t 是为了兼容平台,在64位的机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名;在32位的机器上,intptr_t和uintptr_t分别是int、unsigned int的别名
// DISGUISE 内部对指针做了 ~ 取反操作
disguised_ptr_t disguised_object = DISGUISE(object);
/*
AssociationsHashMap 继承自 unordered_map,存储 key-value 的组合
iterator find ( const key_type& key ),如果 key 存在,则返回key对象的迭代器,
如果key不存在,则find返回 unordered_map::end;因此可以通过 `map.find(key) == map.end()`
判断 key 是否存在于当前 map 中。
*/
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
/*
unordered_map 的键值分别是迭代器的first和second属性。
所以说上面先通过 object 对象(实例对象or类对象) 找到其所有关联对象
i->second 取到又是一个 ObjectAssociationMap
此刻再通过我们自己设定的 key 来查找对应的关联属性值,不过使用
`ObjcAssociation` 封装的
*/
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// 平常 OBJC_ASSOCIATION_RETAIN = 01401
// OBJC_ASSOCIATION_GETTER_RETAIN = (1 << 8)
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// retain the new value (if any) outside the lock.
ObjcAssociation old_association(0, nil);
// 如果value对象存在,则进行retain or copy 操作
id new_value = value ? acquireValue(value, policy) : nil;
{
AssociationsManager manager;
// manager.associations() `AssociationsHashMap` 对象(*_map)
AssociationsHashMap &associations(manager.associations());
// intptr_t 是为了兼容平台,在64位的机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名;在32位的机器上,intptr_t和uintptr_t分别是int、unsigned int的别名
// DISGUISE 内部对指针做了 ~ 取反操作
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
/*
AssociationsHashMap 继承自 unordered_map,存储 key-value 的组合
iterator find ( const key_type& key ),如果 key 存在,则返回key对象的迭代器,
如果key不存在,则find返回 unordered_map::end;因此可以通过 `map.find(key) == map.end()`
判断 key 是否存在于当前 map 中。
*/
AssociationsHashMap::iterator i = associations.find(disguised_object);
// 这里和get操作不同,set操作时如果查询到对象没有关联对象,那么这一次设值是第一次,
// 所以会创建一个新的 ObjectAssociationMap 用来存储实例对象的所有关联属性
if (i != associations.end()) {
// secondary table exists
/*
unordered_map 的键值分别是迭代器的first和second属性。
所以说上面先通过 object 对象(实例对象or类对象) 找到其所有关联对象
i->second 取到又是一个 ObjectAssociationMap
此刻再通过我们自己设定的 key 来查找对应的关联属性值,不过使用
`ObjcAssociation` 封装的
*/
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
// 关联属性用 ObjcAssociation 结构体封装
if (j != refs->end()) {
old_association = j->second;
j->second = ObjcAssociation(policy, new_value);
} else {
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
// 知识点是:newisa.has_assoc = true;
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
if (old_association.hasValue()) ReleaseValue()(old_association);
}
参考内容
-
使用了
policy
设置内存管理策略- OBJC_ASSOCIATION_ASSIGN类型的关联对象和
weak
有一定差别,而更加接近于unsafe_unretained
,即当目标对象遭到摧毁时,属性值不会自动清空。
- OBJC_ASSOCIATION_ASSIGN类型的关联对象和
-
然后内部封装一个
weak
变量持有;或者不用 weak,但是还是封装一层,但是在 dealloc 中进行置为 nil操作
参考内容
- 基于栈为节点(node)的双向链表
- 使用
@autoreleasePool
包裹的作用域中,所有调用 autorelease 都会将对象push到自动释放池,作用域结束就会drain一次,这里涉及到了 哨兵对象,也就是 插入一个nil标识。
参考内容
- 循环引用;
- CF类型内存/C语言malloc出来的对象
- 单例也会造成内存泄漏
- 可以根据项目需求设置为可销毁对象
参考内容
- method_exchangeImplementations -> method_setImplementation
- 如果直接替换,相当于交换了父类这个方法的实现,但这个新的实现是在子类中的,父类的实例调用这个方法时,会崩溃。建议:class_addMethod
AFNetworking 源码涉及代码
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
}
static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method));
}
+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));
if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
}
if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
}
}
参考内容
- unfair 锁,不能完全保证
- 因为对于容器变量的修改不能保证线程安全,比如 NSMutableArray/NSMutableDictionnary。等
参考内容
对象在运行时获取其类型的能力称为内省
- 参考下题
-(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: 判断是否是这个类的实例
-(BOOL) respondsToSelector: 判读实例是否有这样方法
+(BOOL) instancesRespondToSelector: 判断类是否有这个方法
参考内容
- object_getClass:获得的是isa的指向
- self.class:当self是实例对象的时候,返回的是类对象,否则则返回自身。
- 类方法class,返回的是self,所以当查找meta class时,需要对类对象调用object_getClass方法
/ 返回的 isa 指针指向的对象,如果 obj 是实例,返回的是类对象,如果 obj 是类对象,返回的是元类对象
// 如果是元类对象,返回的是 NSObject 元类对象(所有元类对象的isa指针都指向了 NSObject 元类对象)
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
// 通过传入类名称 返回类对象
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;
// NO unconnected, YES class handler
return look_up_class(aClassName, NO, YES);
}
参考内容
- 数据结构关系图
- 通过name & object 查找到所有的obs对象(保存了observer和sel),放到数组中
- 通过performSelector:逐一调用sel,这是个同步操作
- 释放notification对象
// 根容器,NSNotificationCenter持有
typedef struct NCTbl {
Observation *wildcard; /* 链表结构,保存既没有name也没有object的通知 */
GSIMapTable nameless; /* 存储没有name但是有object的通知 */
GSIMapTable named; /* 存储带有name的通知,不管有没有object */
...
} NCTable;
// Observation 存储观察者和响应结构体,基本的存储单元
typedef struct Obs {
id observer; /* 观察者,接收通知的对象 */
SEL selector; /* 响应方法 */
struct Obs *next; /* Next item in linked list. */
...
} Observation;
- 同步发送
- NSNotificationQueue
参考内容
通知队列,用于异步发送消息,这个异步并不是开启线程,而是把通知存到双向链表实现的队列里面,等待某个时机触发时调用NSNotificationCenter的发送接口进行发送通知,这么看NSNotificationQueue 最终还是调用NSNotificationCenter进行消息的分发
- 依赖runloop,所以如果在其他子线程使用NSNotificationQueue,需要开启runloop
- 最终还是通过NSNotificationCenter进行发送通知,所以这个角度讲它还是同步的
- 所谓异步,指的是非实时发送而是在合适的时机发送,并没有开启异步线程
参考内容
- NSNotificationQueue主要做了两件事:
- 添加通知到队列
- 删除通知
参考内容
- 使用addObserverForName: object: queue: usingBlock方法注册通知,指定在mainqueue上响应block
- 在主线程注册一个machPort,它是用来做线程通信的,当在异步线程收到通知,然后给machPort发送消息,这样肯定是在主线程处理的,具体用法去网上资料很多,苹果官网也有
参考内容
-
有时候会导致crash
-
比如在你通知事件中处理数据或UI事件,但是由于通知的的不确定性造成处理事件的时间不确定,有异步操作在通知事件中处理等都可能造成崩溃。
参考内容
-
存储过程并没有做去重操作,同一个通知注册多次则响应多次
-
因为查找时做了这个链表的遍历,所以删除时会把重复的通知全都删除掉
// 发送通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:@"TestNotification" object:@1];
// 接收通知
[NSNotificationCenter.defaultCenter postNotificationName:@"TestNotification" object:nil];
参考内容
- 存储是以name和object为维度的,即判定是不是同一个通知要从name和object区分,如果他们都相同则认为是同一个通知,后面包括查找逻辑、删除逻辑都是以这两个为维度的
- 故而不会。
参考内容
-
系统响应阶段 (SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系统接收到的触摸事件。)
- 指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
- IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。
- SpringBoard进程因接收到触摸事件,将触摸事件交给前台app进程来处理。
-
APP响应阶段
- APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
- source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
- source0回调内部将触摸事件添加到UIApplication对象的事件队列中。事件出队后,UIApplication开始一个寻找最佳响应者的过程,这个过程又称hit-testing,另外,此处开始便是与我们平时开发相关的工作了。
- 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。
- 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么致死也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。
参考内容
-
主线程需要维持一份RunLoop,保持App在Main后不会直接退出。
-
其他线程默认并没有调用
NSRunLoop *runloop = [NSRunLoop currentRunLoop]
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
参考内容
- UIKit并不是一个 线程安全 的类,UI操作涉及到渲染访问各种View对象的属性
- 如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度
- 另一方面因为整个程序的起点
UIApplication
是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以 view 只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。
参考内容
-
perform 有几种方式,如 [self performSelector:@selector(perform) withObject:nil] 同步执行的,等同于 objc_msgSend 方法执行调用方法。
-
[self performSelector:@selector(perform) withObject:nil afterDelay:0] 则是会在当前 runloop 中起一个 timer,如果当前线程没有起runloop(也就是上面说的没有调用 [NSRunLoop currentRunLoop]` 方法的话),则不会有输出
参考内容
线程保活就是不让线程退出,所以往简单说就是搞个 “while(1)” 自己实现一套处理流程,事件派发就可以了。
-
runloop 线程保活前提就是有事情要处理,这里指 timer,source0,source1 事件。
-
Timer && Port
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer 定时任务");
}];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addTimer:timer forMode:NSDefaultRunLoopMode];
[runloop run];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
参考内容
+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
if ([key isEqualToString:@"closeType"]) {
return NO;
}else{
return [super automaticallyNotifiesObserversForKey:key];
}
}
-(void)setProperty:(NSString *)Property{
if (_Property!=Property) {
[self willChangeValueForKey:@"Property"];
_Property=Property;
[self didChangeValueForKey:@"Property"];
}
}
参考内容
会触发 KVO 操作,KVC 时候会先查询对应的 getter 和 setter 方法。
如果返回 YES,那么可以直接修改实例变量。
+ (BOOL)accessInstanceVariablesDirectly {
return NO;
}
-
KVC 调用 getter 流程:
getKEY,KEY,isKEY, _KEY
,接着是实例变量_KEY,_isKEY, KEY, isKEY
; -
KVC 调用 setter 流程:
setKEY
和_setKEY
,实例变量顺序_KEY,_isKEY, KEY, isKEY
,没找到就调用setValue: forUndefinedKey:
参考内容
- dealloc 没有移除 kvo 观察者,解决方案:创建一个中间对象,将其作为某个属性的观察者,然后dealloc的时候去做移除观察者,而调用者是持有中间对象的,调用者释放了,中间对象也释放了,dealloc 也就移除观察者了;
- 多次重复移除同一个属性,移除了未注册的观察者
- 被观察者提前被释放,被观察者在 dealloc 时仍然注册着 KVO,导致崩溃。 例如:被观察者是局部变量的情况(iOS 10 及之前会崩溃) 比如 weak ;
- 添加了观察者,但未实现
observeValueForKeyPath:ofObject:change:context:
方法,导致崩溃; - 添加或者移除时
keypath == nil
,导致崩溃;
参考内容
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
参考内容
一般,block有三种:_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock,根据Block对象创建时所处数据区不同而进行区别。
- 栈上 Block,引用了栈上变量,生命周期由系统控制的,一旦所属作用域结束,就被系统销毁了。
- 堆上 Block,使用 copy 或者 strong(ARC)下就从栈Block 拷贝到堆上。
- 全局 Block,未引用任何栈上变量时就是全局Block;
参考内容
- 值 copy 和指针 copy,
__block
修饰的话允许在 block 内部修改变量,因为传入的是 int变量的指针。 - 外部变量有四种类型:自动变量、静态变量、静态全局变量、全局变量。
- 全局变量和静态全局变量在 block 中是直接引用的,不需要通过结构去传入指针;
- 函数/方法中的 static 静态变量是直接在block中保存了指针,如下测试代码:
参考内容
不需要
- 本身 block 内部就捕获了 NSMutableArray 指针,除非你要修改指针指向的对象,而这里明显只是修改内存数据,这个可以类比 NSMutableString。
参考内容
static void *_Block_copy_internal(const void *arg, const int flags) void _Block_release(void *arg)
- _NSConcreteGlobalBlock:是设置在程序的全局数据区域(.data区)中的Block对象。在全局声明实现的block 或者 没有用到自动变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。
- _NSConcreteStackBlock是设置在栈上的block对象,生命周期由系统控制的,一旦所属作用域结束,就被系统销毁了。
参考内容
- ARC 是可以的
- strong 和 copy 的操作都是将栈上block 拷贝到堆上。
参考内容
-
__weak 就是为了避免 retainCycle
-
而block 内部
__strong
则是在作用域 retain 持有当前对象做一些操作,结束后会释放掉它。
参考内容
-
调用Block的copy方法
-
将Block作为函数返回值时
-
将Block赋值给__strong修饰的变量或Block类型成员变量时
-
向Cocoa框架含有usingBlock的方法或者GCD的API传递Block参数时
参考内容
-
只要block引用外部局部变量,block就放在堆里面,block使用copy,尽量不要使用strong。
-
只要block引用外部局部变量,block放在栈里面,block只能使用copy不能使用retain,用retain,block还是在栈里面。
主要以GCD为主
参考内容
- Pthread,较少使用。
- NSThread,每个 NSThread对象对应一个线程,量级较轻,通常我们会起一个 runloop 保活,然后通过添加自定义source0源或者 perform onThread 来进行调用,优点轻量级,使用简单,缺点:需要自己管理线程的生命周期,保活,另外还会线程同步,加锁、睡眠和唤醒。
- GCD:Grand Central Dispatch(派发) 是基于C语言的框架,可以充分利用多核,是苹果推荐使用的多线程技术
- 优点:GCD更接近底层,而NSOperationQueue则更高级抽象,所以GCD在追求性能的底层操作来说,是速度最快的,有待确认
- 缺点:操作之间的事务性,顺序行,依赖关系。GCD需要自己写更多的代码来实现
- NSOperation
- 优点: 使用者的关注点都放在了 operation 上,而不需要线程管理。
- 支持在操作对象之间依赖关系,方便控制执行顺序。
- 支持可选的完成块,它在操作的主要任务完成后执行。
- 支持使用KVO通知监视操作执行状态的变化。
- 支持设定操作的优先级,从而影响它们的相对执行顺序。
- 支持取消操作,允许您在操作执行时暂停操作。
- 缺点:高级抽象,性能方面相较 GCD 来说不足一些;
- 优点: 使用者的关注点都放在了 operation 上,而不需要线程管理。
参考内容
- 主队列(main queue )【串行】
- 保证所有的任务都在主线程执行,而主线程是唯一用于 UI 更新的线程。此外还用于发送消息给视图或发送通知。
- 四个全局调度队列(high、default、low、background【并发】
- Apple 的接口也会使用这些队列,所以你添加的任何任务都不会是这些队列中唯一的任务
- 自定义队列
- 多个任务以串行方式执行,但又不想在主线程中
- 多个任务以并行方式执行,但不希望队列中有其他系统的任务干扰。
参考内容
- dispatch_barrier_sync && dispatch_barrier_async
- dispatch_once
- dispatch_async
- dispatch_apply
- dispatch_group_t
- 本系列面试题第一份:谈下iOS开发中知道的哪些锁?(dispatch_semaphore部分)
- ...
参考内容
- 队列其实就是一个数据结构体,主队列由于是串行队列,所以入队列中的 task 会逐一派发到主线程中执行;但是其他队列也可能会派发到主线程执行
参考内容
- dispatch_sync
- dispatch_group,
- dispatch_semaphore
- NSLock/NSRecursiveLock
- pthread_mutex_t 互斥锁、递归锁等
- @synchronized
参考内容
void dispatch_once(dispatch_once_t *val, dispatch_block_t block)
{
dispatch_once_f(val, block, _dispatch_Block_invoke(block));
}
void
dispatch_once_f(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
#if !DISPATCH_ONCE_INLINE_FASTPATH
if (likely(os_atomic_load(val, acquire) == DLOCK_ONCE_DONE)) {
return;
}
#endif // !DISPATCH_ONCE_INLINE_FASTPATH
return dispatch_once_f_slow(val, ctxt, func);
}
static void
dispatch_once_f_slow(dispatch_once_t *val, void *ctxt, dispatch_function_t func)
{
_dispatch_once_waiter_t volatile *vval = (_dispatch_once_waiter_t*)val;
struct _dispatch_once_waiter_s dow = { };
_dispatch_once_waiter_t tail = &dow, next, tmp;
dispatch_thread_event_t event;
if (os_atomic_cmpxchg(vval, NULL, tail, acquire)) {
dow.dow_thread = _dispatch_tid_self();
_dispatch_client_callout(ctxt, func);
next = (_dispatch_once_waiter_t)_dispatch_once_xchg_done(val);
while (next != tail) {
tmp = (_dispatch_once_waiter_t)_dispatch_wait_until(next->dow_next);
event = &next->dow_event;
next = tmp;
_dispatch_thread_event_signal(event);
}
} else {
_dispatch_thread_event_init(&dow.dow_event);
next = *vval;
for (;;) {
if (next == DISPATCH_ONCE_DONE) {
break;
}
if (os_atomic_cmpxchgv(vval, next, tail, &next, release)) {
dow.dow_thread = next->dow_thread;
dow.dow_next = next;
if (dow.dow_thread) {
pthread_priority_t pp = _dispatch_get_priority();
_dispatch_thread_override_start(dow.dow_thread, pp, val);
}
_dispatch_thread_event_wait(&dow.dow_event);
if (dow.dow_thread) {
_dispatch_thread_override_end(dow.dow_thread, val);
}
break;
}
}
_dispatch_thread_event_destroy(&dow.dow_event);
}
}
参考内容
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog()
});
参考内容
默认值为 -1
默认的最大操作数由NSOperationQueue对象根据当前系统条件动态确定。
参考内容
- 计时不精确:不管是一次性的还是周期性的timer的实际触发事件的时间,都会与所加入的RunLoop和RunLoop Mode有关,如果此RunLoop正在执行一个连续性的运算,timer就会被延时出发。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会在延时结束后立刻执行,并按照之前指定的周期继续执行。
Apple 专门提供的一个类,主要的优势在于他的执行频率是根据设备屏幕的刷新频率来计算的,也即是时间间隔最准确的定时器。用法和 NSTimer 差不多,当然也存在 retainCycle 问题,解决方式同上。
- 优点: 依托于设备屏幕刷新频率触发事件,所以其触发时间上是最准确的。也是最适合做UI不断刷新的事件,过渡相对流畅,无卡顿感。
- 缺点:
- 由于依托于屏幕刷新频率,若果CPU不堪重负而影响了屏幕刷新,那么我们的触发事件也会受到相应影响。
- selector触发的时间间隔只能是duration的整倍数
- selector事件如果大于其触发间隔就会造成掉帧现象。
- (void) dispatch_source_t {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(self.timer, dispatch_walltime(NULL,0 * NSEC_PER_SEC), 1 * NSEC_PER_SEC, 0);
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"triggered");
});
}
dispatch_source_cancel(self.disTimer);
**注意:**dispatch_source_t 一定要被设置为成员变量,否则将会立即被释放。
-
优缺点
- **优点:**不受当前runloopMode的影响;
- **缺点:**虽然不受runloopMode的影响,但是其计时效应仍不是百分之百准确的;
参考内容
2011年,Apple将Cassowary算法用到了自家的布局引擎AutoLayout中
-
Auto Layout 不仅仅包含布局算法 Cassowary,还包含了布局在运行时的生命周期等一整套布局引擎系统,用来统一管理布局的创建、更新和销毁
-
套布局引擎系统叫作 Layout Engine ,是 Auto Layout 的核心,主导着整个界面布局。 每个视图在得到自己的布局之前,Layout Engine 会将视图、约束、优先级、固定大小通过计算转换成最终的大小和位置。在 Layout Engine 里,每当约束发生变化,就会触发 Deferred Layout Pass(延迟布局传递),完成后进入监听约束变化的状态。当再次监听到约束变化,即进入下一轮循环中。
- Constraints Change 表示的就是约束变化,添加、删除视图时会触发约束变化。Activating 或 Deactivating,设置 Constant 或 Priority 时也会触发约束变化。Layout Engine 在碰到约束变化后会重新计算布局,获取到布局后调用 superview.setNeedLayout(),然后进入 Deferred Layout Pass。Deferred Layout Pass 的主要作用是做容错处理。如果有些视图在更新约束时没有确定或缺失布局声明的话,会先在这里做容错处理。
- 接下来,Layout Engine 会从上到下调用 layoutSubviews() ,通过 Cassowary 算法计算各个子视图的位置,算出来后将子视图的 frame 从 Layout Engine 里拷贝出来。在这之后的处理,就和手写布局的绘制、渲染过程一样了。所以,使用 Auto Layout 和手写布局的区别,就是多了布局上的这个计算过程
- 每一个需要接收到更新约束的 view 会从子 view 向上传递,直到 window; 然后,每一个接收到的 view 开始 layoutsubviews,和更新约束是从相反的方向开始,layout 从 window 开始到每一个子 view 进行 layout。最后,每一个需要渲染的 view,和 layout 相同,从父 view 向子view 开始渲染
- 只需要调用updateConstraints 并指定好要更新的属性,Render Loop会帮助你计算好它的frame并完成渲染,从而避免多次设置的重复工作。
-
iOS 12 使得 Auto Layout 具有了和手写布局几乎相同的高性能。
- iOS 12 之前,很多约束变化时都会重新创建一个计算引擎 NSISEnginer 将约束关系重新加进来,然后重新计算。结果就是,涉及到的约束关系变多时,新的计算引擎需要重新计算,最终导致计算量呈指数级增加。更详细的讲解,你可以参考 WWDC 2018 中 202 Session 的内容,里面完整地分析了以前的问题,以及 iOS12 的解法。总体来说, iOS12 的 Auto Layout 更多地利用了 Cassowary 算法的界面更新策略,使其真正完成了高效的界面线性策略计算。
单一职责原则
-
UIView 为 CALayer 提供内容,以及负责处理触摸等事件,参与响应链;
-
CALayer 负责显示内容 contents
参考内容
参考内容
- 如果在UIView初始化时没有设置frame,会导致drawRect不被自动调用
- sizeToFit后会调用。这时候可以先用sizeToFit中计算出size,然后系统自动调用drawRect方法
- 通过设置contentMode为.redraw时,那么在每次设置或更改frame的时候自动调用drawRect
- 直接调用setNeedsDisplay,或者setNeedsDisplayInRect会触发drawRect
-
init初始化不会触发layoutSubviews。
-
addSubview会触发layoutSubviews。
-
改变一个UIView的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
-
滚动一个UIScrollView引发UIView的重新布局会触发layoutSubviews。
-
旋转Screen会触发父UIView上的layoutSubviews事件。
-
直接调用 setNeedsLayout 或者 layoutIfNeeded。
-
setNeedsLayout
- 标记为需要重新布局,异步调用
layoutIfNeeded
刷新布局,不立即刷新,在下一轮runloop结束前刷新,对于这一轮runloop
之内的所有布局和UI上的更新只会刷新一次,layoutSubviews
一定会被调用。
- 标记为需要重新布局,异步调用
-
layoutIfNeeded 如果有需要刷新的标记,立即调用
layoutSubviews
进行布局(如果没有标记,不会调用layoutSubviews
)。
参考内容
这个题目,谷歌很多详细参考答案,此处略。
- CPU:计算视图frame,图片解码,绘制纹理交给GPU。
- GPU:纹理混合,顶点变换,渲染到帧缓冲区。
- 时钟信号:垂直同步信号V-Sync / 水平同步信号H-Sync。
- iOS设备双缓冲机制:前/后帧缓冲区。
- CRT:阴极电子枪发射电子,在阴极高电压的作用下,电子由电子枪射向荧光屏,使荧光粉发光,将图像显示在屏幕上。采用时钟信号控制。
- LCD:(光学成像原理)在不加电压的情况下,光线会沿着液晶分子的间隙前进旋转90°,光可以通过。在 加入电压后,光沿着液晶分子的间隙直线前进,被滤光板挡住。
- 注:LCD的成像原理与CRT截然不同,每一个像素的颜色在需要改变时才去改变电压,但扔需要按照一定的刷新频率向GPU获取新的图像用于显示。
参考内容
隐式动画,指我们可以在不设定任何动画类型的情况下,仅仅改变CALayer的一个可做动画的属性,就能实现动画效果。
- 事务,其实是Core Animation用来包含一系列属性动画集合的机制,通过指定事务来改变图层的可动画属性,这些变化都不是立刻发生变化的,而是在事务被提交的时候才启动一个动画过渡到新值。任何可以做动画的图层属性都会被添加到栈顶的事务。
- 现在再来考虑隐式动画,其实是Core Animation在每个RunLoop周期中会自动开始一次新的事务,即使你不显式的使用[CATranscation begin]开始一次事务,任何在一次RunLoop运行时循环中属性的改变都会被集中起来,执行默认0.25秒的动画。
//动画属性的入栈
+ (void)begin;
//动画属性出栈
+ (void)commit;
//设置当前事务的动画时间
+ (void)setAnimationDuration:(CFTimeInterval)dur;
//获取当前事务的动画时间
+ (CFTimeInterval)animationDuration;
// 在动画结束时提供一个完成的动作
+ (void)setCompletionBlock:(nullable void (^)(void))block;
如果直接对UIView或者CALayer关联的图层layer改变动画属性,这样是没有隐式动画效果的,这说明虽然Core Animation对所有的CALayer动画属性设置了隐式动画,但UIView把它关联的图层的这个特性给关闭了。 为了更好的理解中一点,我们需要知道隐式动画是如何实现的: 我们把改变属性时CALayer自动执行的动画称作行为,当CALayer的属性被修改时,它会调用-actionForKey:方法传递属性名称
- 图层会首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法;如果有,就直接调用并返回结果。
- 如果没有委托或者委托没有实现-actionForLayer:forKey方法,图层将会检查包含属性名称对应行为映射的actions字典
- 如果actions字典没有包含对应的属性,图层接着在它的style字典里搜索属性名.
- 最后,如果在style也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的+defaultActionForKey:方法
从流程上分析来看,经过一次完整的搜索动画之后,-actionForKey:要么返回空(这种情况不会有动画发生),要么返回遵循CAAction协议的对象(CALayer拿这个结果去对先前和当前的值做动画)。现在我们再来考虑UIKit是如何禁用隐式动画的: 每个UIView对它关联的图层都遵循了CALayerDelegate协议,并且实现了-actionForLayer:forKey方法。当不在一个动画块中修改动画属性时,UIView对所有图层行为都返回了nil,但是在动画Block范围就返回了非空值.
- 当属性在动画块之外发生变化,UIView直接通过返回nil来禁用隐式动画。但是如果在动画块范围内,UIView则会根据动画具体类型返回响应的属性,
当然,返回nil并不是禁用隐式动画的唯一方法,CATransaction也为我们提供了具体的方法,可以用来对所有属性打开或者关闭隐式动画,方法如下:
+ (void)setDisableActions:(BOOL)flag;
-
UIView关联的图层禁用了隐式动画,那么对这种图层做动画的方法有有了以下几种方式:
- 使用UIView的动画函数(而不是依赖CATransaction)
- 继承UIView,并覆盖-actionforLayer:forkey:方法
- 直接创建显式动画
-
其实,对于单独存在的图层,我们也可以通过实现图层的-actionforLayer:forkey:方法,或者提供一个actions字典来控制隐式动画
通过对事务和图层行为的了解,我们可以这样思考,图层行为其实是被Core Animation隐式调用的显式动画对象。我们可以发现改变隐式动画的这种图层行为有两种方式: 1.给layer设置自定义的actions字典 2.实现委托代理,返回遵循CAAction协议的动画对象 现在,我们尝试使用第一种方法来自定义图层行为,这里用到的是一个推进过渡的动画(也是遵循了CAAction的动画类)
参考内容
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染。
- 其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU。
参考内容
- 如果图片较小,并且使用频繁的图片使用 imageNamed:方法来加载
- 相同的图片是不会重复加载的
- 如果图片较大,并且使用较少,使用imageWithContentOfFile:来加载。
- 加载:
imageWithContentsOfFile
只能加载 mainBundle 中图片。
- 加载:
- 当你不需要重用该图像,或者你需要将图像以数据方式存储到数据库,又或者你要通过网络下载一个很大的图像时,使用
imageWithContentsOfFile
; - 如果在程序中经常需要重用的图片,比如用于UITableView的图片,那么最好是选择imageNamed方法。这种方法可以节省出每次都从磁盘加载图片的时间;
参考内容
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。
- 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩
- 然后将生成的 UIImage 赋值给 UIImageView ;
- 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
解码操作是比较耗时的,并且没有GPU硬解码,只能通过CPU,iOS默认会在主线程对图像进行解码。解码过程是一个相当复杂的任务,需要消耗非常长的时间。60FPS ≈ 0.01666s per frame = 16.7ms per frame,这意味着在主线程超过16.7ms的任务都会引起掉帧。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。
-
当加载图片的时候,iOS通常会延迟解压图片的时间,直到加载到内存之后。因为需要在绘制之前进行解压,这就会在准备绘制图片的时候影响性能。
-
iOS通常会延时解压图片,等到图片在屏幕上显示的时候解压图片。解压图片是非常耗时的操作。
-
通过 imageNamed 创建 UIImage 时,当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,并且内存中自动缓存解压后的图片。当APP第一次退到后台和收到内存警告时,缓存才会被自动清空。
-
这个方法不会缓存解压后的图片,也就是说每次调用时都会对文件进行加载和解压。iOS通常会延迟解压图片,为了提升性能,在屏幕绘制前可以强制解压。
- 将图片的一个像素绘制成一个像素大小的CGContext。这样仍然会解压整张图片,但是绘制本身并没有消耗任何时间。这样的好处在于加载的图片并不会在特定的设备上为绘制做优化,所以可以在任何时间点绘制出来。同样iOS也就可以丢弃解压后的图片来节省内存了。
- Large Image Downsizing:SDWebImage解码的方法在SDWebImageDecoder这个类里。这个类里有两个方法,decodedImageWithImage是直接对图片解码,decodedAndScaledDownImageWithImage这个方法里会先判断图片的要解压缩的图片大小是否超过60M,没超过的话会调用decodedImageWithImage这个方法直接解码图片,否则会对原图片进行缩放以减少占用内存空间,并且解码图片时会把原始的图片数据分成多个tail进行解码。
参考内容
- 绘制到 UIGraphicsImageRenderer 上
- 绘制到 Core Graphics Context 上
- 使用 Image I/O 创建缩略图像
- 使用 Core Image 进行 Lanczos 重采样
- 使用 vImage 优化图片渲染
这块内容较多,限于篇幅不会太详细回复,针对不同方向可研究&&实践等。
参考内容
- 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
- 检查下framework应当设为optional或required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为`optional``会有些额外的检查;
- 合并或者删减一些OC类和函数;关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高)。
- 删减一些无用的静态变量,
- 删减没有被调用到或者已经废弃的方法。
- 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
- 类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来;
- 用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;
- 在设计师可接受的范围内压缩图片的大小,会有意外收获。压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是TinyPNG。
参考内容
参考内容
参考内容
在 iOS 平台对第三方 APP 有绝对的控制权,一定要保证每一个安装到 iOS 上的 APP 都是经过苹果官方允许的,场景有如下三种
- AppStore 下载应用验证,传 App 上 AppStore 时,苹果后台用私钥对 APP 数据进行签名,iOS 系统下载这个 APP 后,用公钥验证这个签名,若签名正确,这个 APP 肯定是由苹果后台认证的,并且没有被修改过,也就达到了苹果的需求:保证安装的每一个 APP 都是经过苹果官方允许的。
- 开发 App 时可以直接把开发中的应用安装进手机进行调试。
- In-House 企业内部分发,可以直接安装企业证书签名后的 APP。
- AD-Hoc 相当于企业分发的限制版,限制安装设备数量,较少用。
参考内容
- 由苹果生成一对公私钥,公钥内置与iOS设备中,私钥由苹果保管。
- 开发者上传App给苹果审核后,苹果用私钥对App数据进行签名,发布至App Store。
- iOS设备下载App后,用公钥进行验证,若正确,则证明App是由苹果认证过的。
参考内容
由于不需要提交苹果审核,所以苹果没办法对App进行签名,因此苹果采用了双重签名的机制。Mac电脑有一对公私钥,苹果还是原来的一对公私钥。
- 开发时需要真机测试时,需要从钥匙串中的证书中心创建证书请求文件(CSR),并传至苹果服务器。
- Apple使用私钥对 CSR 签名,生成一份包含Mac公钥信息及Apple对它的签名,被称为证书(CER:即开发证书,发布证书)。
- 编译完一个App后,Mac电脑使用私钥对App进行签名。
- 在安装App时,根据当前配置把CER证书一起打包进App。
- iOS设备通过内置的Apple的公钥验证CER是否正确,证书验证确保Mac公钥时经过苹果认证的。
- 再使用CER文件中Mac的公钥去验证App的签名是否正确,确保安装行为是经过苹果允许的。
Xcode打包App生成ipa文件,通过iTunes或者蒲公英等第三方发布平台,安装到手机上。流程步骤基本和真机调试相同,差别在于第4步:
- 开发时需要打包测试或发布时,需要从钥匙串中的证书中心创建证书请求文件(CSR),并传至苹果服务器。
- Apple使用私钥对 CSR 签名,生成一份包含Mac公钥信息及Apple对它的签名,被称为证书(CER:即开发证书,发布证书)。
- 编译完一个App后,Mac电脑使用私钥对App进行签名。
- 编译签名完之后,要导出ipa文件,导出时,需要选择一个保存的方法(App Store/Ad Hoc/Enterprise/Development),就是选择将上一步生成的CER一起打包进App。
- iOS设备通过内置的Apple的公钥验证CER是否正确,证书验证确保Mac公钥是经过苹果认证的。
- 再使用CER文件中Mac的公钥去验证App的签名是否正确,确保安装行为是经过苹果允许的。
典型源码的学习
只是列出一些iOS比较核心的开源库,这些库包含了很多高质量的思想,源码学习的时候一定要关注每个框架解决的核心问题是什么,还有它们的优缺点,这样才能算真正理解和吸收
- AFN
- SDWebImage
- JSPatch、Aspects(虽然一个不可用、另一个不维护,但是这两个库都很精炼巧妙,很适合学习)
- Weex/RN, 笔者认为这种前端和客户端紧密联系的库是必须要知道其原理的
- CTMediator、其他router库,这些都是常见的路由库,开发中基本上都会用到
参考内容
参考内容
-
MVP模式是MVC模式的一个演化版本(好像所有的模式都是出自于MVC~~),MVP全称Model-View-Presenter。
- Presenter:作为model和view的中间人,从model层获取数据之后传给view,使得View和model没有耦合。
- 说了那么多,总得来说MVP的好处就是解除view与model的耦合,使得view或model有更强的复用性。
参考内容
参考内容
-
由于单例模式,不是抽象的所以可扩展性比较差;
-
单例类,职责过重,在一定程度上;违背了单一职责
-
滥用单例将带来一些负面的问题,如为了节省资源将数据库连接池对象设计为单例模式,可能会导致共享连接池对象的程序过多未出而出现的连接池溢出,如果实例化对象长时间不用系统就会被认为垃圾对象被回收,这将导致对象状态丢失。
参考内容
参考内容
-
Code Review
-
工作流程
参考内容
-
LRU
- 双向链表(Doubly Linked List)
- 哈希表(Dictionary)
-
一个Cache的操作无非三种:插入、替换、查找
- 插入:当Cache未满时,新的数据项只需插到双链表头部即可
- 替换:当Cache已满时,将新的数据项插到双链表头部,并删除双链表的尾结点即可
- 查找:每次数据项被查询到时,都将此数据项移动到链表头部
-
YYCache YYMemoryCache
- _YYLinkedMapNode
- _YYLinkedMap
-
LRU 参考代码
type LRUCache struct {
size,capacity int
cache map[int] *LinkedNode
head,tail *LinkedNode
}
type LinkedNode struct {
key, value int
prev, next *LinkedNode
}
func initLinkedNode(key, value int)*LinkedNode {
return &LinkedNode {
key:key,
value:value,
}
}
func Construct(capacity int)LRUCache {
l := LRUCache {
size: 0,
capacity:capacity,
cache:map[int]*LinkedNode {},
head:initLinkedNode(0,0),
tail:initLinkedNode(0,0)
}
l.head.next = l.tail
l.tail.prev = l.head
return l
}
func (this *LRUCache)removeNode(node *LinkedNode){
node.prev.next = node.next
node.next.prev = node.prev
}
func (this *LRUCache)addTohead(node *LinkedNode){
node.prev = this.head
node.next = this.head.next
this.head.next.prev = node
this.head.next = node
}
func (this *LRUCache)moveTohead(node *LinkedNode){
this.removeNode(node)
this.addTohead(node)
}
func (this *LRUCache) Get(ket int)int {
if _,ok := this.cache[key]; !ok {
return -1;
}
node := this.cache[key]
this.moveTohead(node)
return node.value
}
func (this *LRUCache) removeTail() *LinkedNode{
node := this.tail.prev
this.removeNode(node)
return node
}
func (this *LRUCache) put(key, value int){
if _,ok := this.cache[key]; !ok{
node := initLinkedNode(key,value);
this.cache[key] = node
this.addTohead(node)
this.size++
if this.size > this.capacity {
removed := this.removeTail()
delete(this.cache, remove.key)
this.size--
}
} else {
node := this.cache[key]
node.value = value
this.moveTohead(node)
}
}
参考内容
- TODO
参考内容
dispatch_semaphore
- 使用信号量机制可以实现线程的同步,也可以控制最大并发数。以下是控制最大并发数的代码
参考内容
- TODO
参考内容
NSInvocation与PerformSelector:的作用是一样的,都是可以直接调用某个对象的消息
- 相同点: 有相同的父类NSObject
- 区别: 在参数个数<= 2的时候performSelector:的使用要简单一些,但是在参数个数 > 2的时候NSInvocation就简单一些
参考内容
Aspects
-
通过组合实现“多继承”
-
通过协议实现“多继承”
-
通过category实现“单继承”(大部分网上文章将此方法误解成“多继承”)
-
面向切面:打点等场景
参考内容
-
Crash case
- unrecognized selector sent to instance 方法找不到
- 数组越界,插入空值
- [NSDictionary initWithObjects:forKeys:]使用此方法初始化字典时,objects和keys的数量不一致时
- NSMutableDictionary,setObject:forKey:或者removeObjectForKey:时,key为nil
- setValue:forUndefinedKey:,使用KVC对对象进行存取值时传入错误的key或者对不可变字典进行赋值
- NSUserDefaults 存储时key为nil
- 对字符串操作时,传递的下标超出范围,判断是否存在前缀,后缀子串时,子串为空
- 使用C字符串初始化字符串时,传入null
- 对可变集合或字符串使用copy修饰并进行修改操作
- 在空间未添加到父元素上之前,就使用autoLayout进行布局
- KVO在对象销毁时,没有移除KVO或者多次移除KVO
- 野指针访问
- 死锁
-
代码层加强数据校验
-
Hook方式(数组,字典等)等
参考内容
- 读取可执行文件(Mach-O文件),从Mach-O文件中找到动态链接库dyld的地址,把dyld加载进来。
- dyld先设置一下运行环境,配置环境变量,然后加载可执行文件和依赖动态库。
- 对可执行文件和相关动态库进行链接--用于修证符号指针,使其指向正确地址。
- 递归的对相关动态库进行初始化,对可执行文件初始化,这个过程中会注册Objc类;把category定义的各种方法、属性、协议等加入类的数组中;调用各个类的load方法。
- dyld返回一个主程序的main()函数,开始执行main()函数。
参考内容
- 文件头 mach64 Header
- 加载命令 Load Commands
- 文本段 __TEXT
- 数据段 __DATA
- 动态库加载信息 Dynamic Loader Info
- 入口函数 Function Starts
- 符号表 Symbol Table
- 动态库符号表 Dynamic Symbol Table
- 字符串表 String Table
- 补充:每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间
参考内容
定义
-
进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
-
线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
一个线程只能属于一个进程,但是一个进程可以拥有多个线程。多线程处理就是允许一个进程中在同一时刻执行多个任务。
-
线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小。
-
线程没有地址空间,线程包含在进程的地址空间中。线程上下文只包含一个堆栈、一个寄存器、一个优先权,线程文本包含在他的进程 的文本片段中,进程拥有的所有资源都属于线程。所有的线程共享进程的内存和资源。 同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段, 寄存器的内容,栈段又叫运行时段,用来存放所有局部变量和临时变量。
-
父和子进程使用进程间通信机制,同一进程的线程通过读取和写入数据到进程变量来通信。
-
进程内的任何线程都被看做是同位体,且处于相同的级别。不管是哪个线程创建了哪一个线程,进程内的任何线程都可以销毁、挂起、恢复和更改其它线程的优先权。线程也要对进程施加控制,进程中任何线程都可以通过销毁主线程来销毁进程,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程。
-
子进程不对任何其他子进程施加控制,进程的线程可以对同一进程的其它线程施加控制。子进程不能对父进程施加控制,进程中所有线程都可以对主线程施加控制。
- 进程和线程都有ID/寄存器组、状态和优先权、信息块,创建后都可更改自己的属性,都可与父进程共享资源、都不鞥直接访问其他无关进程或线程的资源。
参考内容
Session ID 缓存和 Session Ticket 里面保存的也是主密钥,而不是会话密钥,这样每次会话复用的时候再用双方的随机数和主密钥导出会话密钥,从而实现每次加密通信的会话密钥不一样,即使一个会话的主密钥泄露了或者被破解了也不会影响到另一个会话。
- 客户端将TLS版本,支持的加密算法,ClientHello random C 发给服务端【客户端->服务端】
- 服务端从加密算法中pick一个加密算法, ServerHello random S,server 证书返回给客户端;【服务端->客户单】
- 客户端验证 server 证书【客户端】
- 客户端生成一个 48 字节的预备主密钥,其中前2个字节是 Protocol Version,后46个字节是随机数,客户端用证书中的公钥对预备主密钥进行非对称加密后通过 client key exchange 子消息发给服务端【客户端->服务端】
- 服务端用私钥解密得到预备主密钥;【服务端】
- 服务端和客户端都可以通过预备主密钥、ClientHello random C 和 ServerHello random S 通过 PRF 函数生成主密钥;会话密钥由主密钥、SecurityParameters.server_random 和 SecurityParameters.client_random 数通过 PRF 函数来生成会话密钥里面包含对称加密密钥、消息认证和 CBC 模式的初始化向量,对于非 CBC 模式的加密算法来说,就没有用到这个初始化向量。
参考内容
-
中间人拦截客户端消息,然后再发送给服务端;服务端发发送消息给中间人,中间人再返还给客户端。
-
HTTP 明文传输,客户端和服务端进行通信时,中间人即指夹在客户端和服务端之间的第三者,对于客户端来说,中间人就是 服务端,对于服务端来说,中间人就是 客户端。
-
使用 HTTPS,单双向认证,HTTPDNS ,直连等
参考内容
三次握手:
为了确认服务端和客户端双方的收发能力(解决超时问题)
- 客户端发送 SYN = 1,seq=x 给服务端
- 服务端接收发送 SYN = 1,ACK = 1,ack=x+1, seq = y 给客户端
- 客户端发送 ACK = 1,ack = y+1 ,seq = z 给服务端
四次挥手:
全双工模式下,二次并无真正关闭,需要四次。
- 主动方发送 FIN = 1,seq = u 给被动方;
- 被动方 ACK = 1,ack = u+1,seq = v;
- 被动方继续传输数据给主动方;
- 被动方没有更多数据了,发送 FIN = 1,ACK=1,seq = w,ack=u+1;
- 主动方 ACK = 1,seq = u + 1,ack = w +1;
参考内容
堆
-
栈区:由编译器自动分配释放,存放函数的参数值,局部变量值等;
-
堆区:一般由程序员分配释放(使用new/delete或malloc/free),若程序员不释放,程序结束时可能由OS回收;
-
栈区:操作方式类似于数据结构中的栈;
-
堆区:不同于数据结构中的堆,分配方式类似于链表。
-
栈区:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出;
-
堆区:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。
-
栈区:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
-
堆区:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
-
栈区:系统自动分配,速度较快。但程序员是无法控制的。
-
堆区:由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.
-
注意:在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一快内存,虽然用起来最不方便。但是速度快,也最灵活。
-
栈区:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
-
堆区:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。
参考内容
对称加密指的就是加密和解密使用同一个秘钥,所以叫做对称加密。对称加密只有一个秘钥,作为私钥。
具体算法有:DES,3DES,TDEA,Blowfish,RC5,IDEA。常见的有:DES,AES,3DES等等。
优点:算法公开、计算量小、加密速度快、加密效率高。 缺点:秘钥的管理和分发非常困难,不够安全。在数据传送前,发送方和接收方必须商定好秘钥,然后双方都必须要保存好秘钥,如果一方的秘钥被泄露,那么加密信息也就不安全了。另外,每对用户每次使用对称加密算法时,都需要使用其他人不知道的唯一秘钥,这会使得收、发双方所拥有的钥匙数量巨大,密钥管理成为双方的负担。
非对称加密指的是:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。 私钥只能由一方安全保管,不能外泄,而公钥则可以发给任何请求它的人。非对称加密使用这对密钥中的一个进行加密,而解密则需要另一个密钥。
我们常见的数字证书、加密狗即是采用非对称加密来完成安全验证的。
优点:安全性更高,公钥是公开的,秘钥是自己保存的,不需要将私钥给别人。 缺点:加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
主要算法:RSA、Elgamal、背包算法、Rabin、HD,ECC(椭圆曲线加密算法)。常见的有:RSA,ECC
网银比较流行的时候,银行给我们发一个动态令牌。这个令牌并不使用任何对称或者非对称加密的算法,在整个银行的认证体系中,动态令牌只是一个一次性口令的产生器,它是基于时间同步方式,每隔60秒产生一个随机6位动态密码在其中运行的主要计算仅包括时间因子的计算和散列值的计算。
在用户从银行手中拿到动态口令令牌卡的时候,在令牌卡的内部已经存储了一份种子文件(即图中钥匙所代表的seed),这份种子文件在银行的服务器里保存的完全一样的一份,所以对于动态口令令牌来说,这种方式是share secret的。另外在令牌硬件上的设置中,假使有人打开了这个令牌卡,种子文件将会从令牌卡的内存上擦除(待考证)。 令牌卡中有了种子文件,并实现了TOTP算法,在预先设置的间隔时间里它就能不断产生不同的动态口令,并显示到屏幕上,而银行服务器上跟随时间做同样的计算,也会得到和令牌卡同样的口令,用作认证。 那么TOTP算法具体做了什么操作呢?在RFC6238中有详细的算法描述,这里也会做简单的叙述。
TOTP是来自 HOTP [RFC4226] 的变形,从统筹上看,他们都是将数据文件进行散列计算,只是HOTP的因子是事件因子,TOTP将因子换成了时间因子,具体的TOTP计算公式(其中的HMAC-SHA-256也可能是 HMAC-SHA-512): TOTP = Truncate(HMAC-SHA-256(K,T))
其中: K 为这里的种子文件内容; T 为计算出来的时间因子 公式中的 HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。而公式中给出的哈希算法是 SHA-256,这种哈希算法目前并没有好的破解办法。 令牌卡中预先设置了要显示的口令长度,TOTP 中的 Truncate 操作剪切获得口令。 以上就是动态口令令牌卡的内部原理。
解答出自zhuanlan.zhihu.com/p/38307899
参考内容
- 签名算法,SHA(Security Hash Algorithm) , MD5 更高效,花费时间更少,但相对较容易碰撞
- SHA1 已经被攻破,所以安全性不行。
参考内容
截获真实客户端的HTTPS请求,伪装客户端向真实服务端发送HTTPS请求
接受真实服务器响应,用Charles自己的证书伪装服务端向真实客户端发送数据内容
-
中间人对于客户端来说就是一个”服务端“;而对于服务端则就是”客户端“角色;
-
客户端将请求发送给中间人,中间人原封不动的把请求递交给服务端;服务端 response 给中间人,中间人在原封不动地将数据回给客户端,此刻中间人不过是个代理,仅仅只是充当了一个递交、转发的角色;
-
对于 HTTP 明文传输,中间人自然可以查看,顺便说一句 DNS 查询时候,自然也是可以截获修改的,TCP/UDP 这种协议无法保证安全性;而对于 HTTPS 中间人就无法查看加密数据,因为客户端和服务端通信时候数据都是加密的(对称加密),密钥协商阶段是非对称加密;
-
像 Charles/Fiddler 抓包原理实际上就是客户端要信任它们的证书(这个是自签的根证书,有兴趣可以看下自建CA 为服务器部署https),现在Charles 会为客户端访问的每个域名都用上面的根证书颁发一个证书,当客户端和 Charles 通信时,先 TCP 三次握手建立连接,然后 SSL 四次握手阶段密钥协商,因为 Charles 自签的根证书已经被信任,所以它颁发的那些域名证书自然也是被信任的(有兴趣自己打开 chrome 的证书管理,确实 Charles 根证书在抓包时为每个域名都颁发了一个证书),所以在密钥协商阶段中的证书校验也是 OK 的,客户端和 Charles 实际上也是通信加密了,但是由于对称密钥就是 Charles 和客户端协商得到的,Charles 自己做加密、解密操作玩罢了。
-
4G网络如何抓包
- iphone安装stream
- mac+iphone,在mac上安装wireshark,这款软件很强大
- 手机开热点,电脑连接手机热点
对于移动开发者来说,一般不会遇到非常难的算法,大多以数据结构为主,笔者列出一些必会的算法,当然有时间了可以去LeetCode上刷刷题
- 八大排序算法
- 栈&队列
- 字符串处理
- 链表
- 二叉树相关操作
- 深搜广搜
- 基本的动态规划题、贪心算法、二分查找
- 面试题系列目录
- 上一份: 新浪公司iOS面试题2019年6月
- 下一份: 出一套iOS高级面试题J_Knight_