iOS 学习知识点杂项

iOS 学习知识点杂项 可能有错误 学习还得靠自己

1. Category

Category(分类)主要作用是为已经存在的类添加方法。Category 可以做到在既不子类化,也不侵入一个类的源码的情况下,为原有的类添加新的方法,从而实现扩展一个类或者分离一个类的目的。在日常开发中我们常常使用 Category 为已有的类扩展功能。

虽然继承也能为已有类增加新的方法,而且还能直接增加属性,但继承关系增加了不必要的代码复杂度,在运行时,也无法与父类的原始方法进行区分。所以我们可以优先考虑使用自定义 Category。

通常 Category(分类)有以下几种使用场景:
把类的不同实现方法分开到不同的文件里。
声明私有方法。
模拟多继承。
将 framework 私有方法公开化。

typedef struct category_t*Category;
structcategory_t{
    constchar*name;                     // 类名
    classref_tcls;                          // 类,在运行时阶段通过 clasee_name(类名)对应到类对象
    structmethod_list_t*instanceMethods;    // Category 中所有添加的对象方法列表
    structmethod_list_t*classMethods;       // Category 中所有添加的类方法列表
    structprotocol_list_t*protocols;            // Category 中实现的所有协议列表
};

Category(分类)中的的方法、属性、协议附加到类上的操作,是在 + load方法执行之前进行的。也就是说,在 + load方法执行之前,类中就已经加载了 Category(分类)中的的方法、属性、协议。
而 Category(分类)和 Class(类)的 + load方法的调用顺序规则如下所示:
先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
调用完主类,再调用分类,按照编译顺序,依次调用;ıÏÏ

  • load方法除非主动调用,否则只会调用一次。
    通过这样的调用规则,我们可以知道:主类的 + load方法调用一定在分类 + load方法调用之前。但是分类 + load方法调用顺序并不不是按照继承关系调用的,而是依照编译顺序确定的,这也导致了 + load方法的调用顺序并不一定确定。

2. Autoreleasepool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

1. Autoreleasepool所使用的数据结构是什么?

autoreleasepool 是没有单独的内存结构的,它是通过以AutoreleasePoolPage 为结点的双向链表来实现的,当调用[NSObject autorelease]时候,会在autorelease前面添加objc_autoreleasePoolPush(),在后面添加objc_autoreleasePoolPop()。

2. AutoreleasePoolPage结构体

magic 用来校验 AutoreleasePoolPage 的结构是否完整;
next 指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin() ;
thread 指向当前线程;
parent 指向父结点,第一个结点的 parent 值为 nil ;
child 指向子结点,最后一个结点的 child 值为 nil ;
depth 代表深度,从 0 开始,往后递增 1;
hiwat 代表 high water mark 。
另外,当 next == begin() 时,表示 AutoreleasePoolPage 为空;当 next == end() 时,表示 AutoreleasePoolPage 已满。

3. 线程的autoreleasepool释放时机。

autoreleasepool是在runloop结束或者休眠的时候释放的。在runloop休眠的时候,会调用pop和push方法,先释放旧的缓存池,再创建新的缓存池,在runloop结束的时候,直接pop缓存池,释放对象。
而在push的时候会将release对象添加到autoreleasepoolpage中next指针指向位置,同时还添加一个哨兵POOL_SENTINEL标记添加到缓存池的对象,当pop的时候,会将next到哨兵POOL_SENTINEL的对象都释放掉。

4. 自己添加的autoreleasepool释放时机。

出了大括号就release。


3. 锁

临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。
互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是semaphore在仅取值0/1时的特例。信号量可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。
条件锁:就是条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。

PS:
信号量(semaphore)

#ifndef WT_LOCK_CREATE
#define WT_LOCK_CREATE(lock) lock = dispatch_semaphore_create(1);
#endif

#ifndef WT_LOCK_LOCK
#define WT_LOCK_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif

#ifndef WT_LOCK_UNLOCK
#define WT_LOCK_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif

4. App启动过程

1:解析Info.plist
加载相关信息,例如如闪屏
沙箱建立、权限检查
2:Mach-O加载
如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法
C++静态对象加载、调用ObjC的 +load 函数
3:程序执行
调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching


5. 无痕埋点

无痕埋点就是记录所有的事件。
用户点击事件;
按钮的点击 UIApplication sendAction:to: from:forEvent:
手势操作 UIGestureRecognizer initWithTarget:action: addTarget:action:
列表点击 UITableView和UICollectionView setDelegate:、tableView:didSelectRowAtIndexPath:、 collectionView:didSelectItemAtIndexPath:等
系统弹窗 UIAlertView setDelegate:、alertView:clickedButtonAtIndex:

非点击事件
页面viewDidLoad 、viewWillAppear: 、viewDidAppear: 、viewWillDisappear

target:action:


6. iOS 常见设计模式

单例:UIApplication;
观察者模式:KVO;
类簇:NSNumber;
装饰者模式:分类;
命令模式:NSInvocation;
享元模式:UITableviewCell(UITableview的重用)


7. iOS 中内省的几个方法 class方法和objc_getClass方法有什么区别

判断对象类型:
-(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: 判断是否是这个类的实例
判断对象or类是否有这个方法
-(BOOL) respondsToSelector: 判读实例是否有这样方法
+(BOOL) instancesRespondToSelector: 判断类是否有这个方法

object_getClass:获得的是isa的指向
self.class:当self是实例对象的时候,返回object_getClass。
类方法class,返回的是self,所以当查找meta class时,需要对类对象调用object_getClass方法


8. 反射

反射是指计算机程序在运行时可以访问、检测和修改它本身状态或行为的一种能力。

例如:
通过反射机制简单实现控制器跳转

// SEL和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromSelector(SEL aSelector);
FOUNDATION_EXPORT SEL NSSelectorFromString(NSString *aSelectorName);
// Class和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromClass(Class aClass);
FOUNDATION_EXPORT Class __nullable NSClassFromString(NSString *aClassName);
// Protocol和字符串转换
FOUNDATION_EXPORT NSString *NSStringFromProtocol(Protocol *proto);
FOUNDATION_EXPORT Protocol * __nullable NSProtocolFromString(NSString *namestr);

9. kvo和kvc

KVO
键值监听,是观察者模式,用于监听属性的改变,主要方法包括:
添加监听 - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
移除监听 - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context;
监听回调 - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;

原理
KVO 通过isa-swizzling(类型混合指针)机制来实现,对象在被addObserver后,isa 指针指向了派生出的新类“NSKVONotifying_X元对象名X”,这个类继承自原类,并重写类被观察对象的的 Set 方法(原对象必须有该属性的 Set 方法),在新 Set 方法中添加了- (void)willChangeValueForKey:(NSString *)key;- (void)didChangeValueForKey:(NSString *)key;等方法以回调给观察者。
手动触发回调,则需要调用- (void)willChangeValueForKey:(NSString *)key;或- (void)didChangeValueForKey:(NSString *)key;

可以通过方法屏蔽某Key自动生成的Set方法,而使用自己的Set方法触发回调

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }else{
        return [super automaticallyNotifiesObserversForKey:key];
    }
}
-(void)setName:(NSString *)name{
    
    if (_name!=name) {
        
        [self willChangeValueForKey:@"name"];
        _name=name;
        [self didChangeValueForKey:@"name"];
    }
}

❏ KVC
通过Key名直接访问对象的属性,或者给对象的属性赋值.

  • (void)setValue:(nullable id)value forKey:(NSString *)key;
    接受者会按照一定顺序搜索设置属性方法(搜索 setKey, _key, _isKey, key, isKey);
    如果未找到设置方法,则会调用-setValue:forUndefinedKey:方法返回NSUndefinedKeyException异常;
    如果找到方法,但是设置参数为 nil 或者类型不匹配,则会调用setNilValueForKey:方法并返回NSInvalidArgumentException异常。

  • (nullable id)valueForKey:(NSString *)key;
    接受者按照getKey:、key、isKey顺序搜索获取属性值方法;
    如果未找到方法,则调用+accessInstanceVariablesDirectly方法查看类是否允许按照_key, _isKey, key, isKey顺序读取;
    如果不允许或者最终未找到,则调用-valueForUndefinedKey方法,并返回NSUndefinedKeyException异常。


10. 关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?

可以不改变源码的情况下增加实例变量。
可与分类配合使用,为分类增加属性.
AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。这相当于把所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址,而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。
销毁

dealloc->rootDealloc->object_dispose->检查有无关联对象,有的话_object_remove_assocations删除


11. weak原理

weak 对象弱引用
Runtime 维护了一个weak哈希表,用于存储指向某个对象的所有weak指针,Key是所指对象的地址,Value是weak指针的地址数组。

阶段:
1:objc_initWeak,初始化新的weak指针指向对象的地址。
2:objc_storeWeak,添加引用时,更新指针指向,创建对应的弱引用表。
3:clearDeallocating,根据对象地址获取weak指针地址的数组,遍历数组将其中的数据设为nil,将对象地址从weak表中删除。

sideTables(64个元素长度的hash数组)
sideTable->{
spinlock,
RefcountMap,(hash map,其key是obj的地址,而value,则是obj对象的引用计数)
weak_table_t(以obj地址为key,弱引用obj的指针的地址作为value的hash表。hash表的节点类型是weak_entry_t)
}
struct weak_table_t {
    weak_entry_t *weak_entries;        // hash数组,用来存储弱引用对象的相关信息weak_entry_t
    size_t    num_entries;             // hash数组中的元素个数
    uintptr_t mask;                    // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)
    uintptr_t max_hash_displacement;   // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};


12. atomic

atomic原子属性

Set 方法:——reallySetProperty(…)

objc_retain(newValue);
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = newValue;
slotlock.unlock();
objc_release(oldValue);

Get 方法:——objc_getProperty(…)

spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(oldValue);
slotlock.unlock();
return objc_autoreleaseReturnValue(value);

而其中
spinlock_t锁实际为

using spinlock_t = mutex_tt;
而mutex_tt为
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
}

其内部是os_unfair_lock,iOS 10之后苹果推荐使用os_unfair_lock来代替不在安全的OSSpinLock

Atomic 是否线程安全?
Atomic 并不能保证线程安全,它只能提升正确率,atomic只是在属性的 Get/Set方法中赋值时添加了锁;
A,B,C三个线程同时发起修改访问属性时,并不能完全保证 A 线程写后读取到的值是 A 写入的值,
也无法保证直接使用 _xxx方式 访问实例变量,与使用 self. 的 Get/Set方法访问属性间获得正确的值。


13. 消息转发

1: 消息动态解析

  • (BOOL)resolveInstanceMethod:(SEL)sel;
  • (BOOL)resolveClassMethod:(SEL)sel;
    添加函数实现(配合class_addMethod())

2:消息接受者重定向

  • (id)forwardingTargetForSelector:(SEL)aSelector;
    返回新的接收者

3:消息重定向

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
  • (void)forwardInvocation:(NSInvocation *)anInvocation;

14. assign、retain、strong、weak、copy

assign:用于对基本数据类型进行赋值操作,不更改引用计数
也可以用来修饰对象,但是,被assign修饰的对象在释放后,指针的地址还是存在的,也就是说指针并没有被置为nil,成为野指针。如果后续在分配对象到堆上的某块内存时,正好分到这块地址,程序就会crash。之所以可以修饰基本数据类型,因为基本数据类型一般分配在栈上,栈的内存会由系统自动处理,不会造成野指针。

weak:修饰Object类型,修饰的对象在释放后,指针地址会被置为nil,是一种弱引用。在ARC环境下,为避免循环引用,往往会把delegate属性用weak修饰;在MRC下使用assign修饰。weak和strong不同的是:当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。

retain和strong:都是强引用,基本可以通用的。
在修饰block属性的时候,相信大家都知道要用copy吧,因为如果不copy的话,block是存放在栈连里面的,他的生命周期会随着函数的结束而出栈的,copy之后会放在堆里面。
strong在修饰block的时候就相当于copy,而retain修饰block的时候就相当于assign,这样block会出现提前被释放掉的危险。

copy:会在内存里拷贝一份对象,两个指针指向不同的内存地址。一般用来修饰NSString等有对应可变类型的对象,因为他们有可能和对应的可变类型(NSMutableString)之间进行赋值操作,为确保对象中的字符串不被修改 ,应该在设置属性是拷贝一份。而若用strong修饰,如果对象在外部被修改了,会影响到属性。

PS:
block属性为什么需要用copy来修饰?

因为在MRC下,block在创建的时候,它的内存是分配在栈(stack)上的,而不是在堆(heap)上,可能被随时回收。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。通过copy可以把block拷贝(copy)到堆,保证block的声明域外使用。在ARC下写不写都行,编译器会自动对block进行copy操作。

__block与__weak的区别

__block:在ARC和MRC下都可用,可修饰对象,也可以修饰基本数据类型。

__block对象可以在block被重新赋值,__weak不可以。

__weak:只在ARC中使用,只能修饰对象,不能修饰基本数据类型(int、bool)。

同时,在ARC下,要避免block出现循环引用,经常会:__weak typedof(self) weakSelf = self;

copy:
对象的复制就是复制⼀个对象作为副本,他会开辟⼀块新的内存(堆内存)来存储副本对象,就像复制⽂件⼀样,即源对象和副本对象是两块不同的内存区域。对象要具备复制功能,必须实现
协议或者协议,常⽤的可复制对象有:
NSNumber、NSString、NSMutableString、NSArray、
NSMutableArray、NSDictionary、NSMutableDictionary
copy:产⽣对象的副本是不可变的
mutableCopy:产⽣的对象的副本是可变的
浅拷贝值复制对象本身,对象⾥的属性、包含的对象不做复制
深拷⻉贝则既复制对象本身,对象的属性也会复制⼀份Foundation中⽀持复制的类,默认是浅复制对象的⾃定义拷⻉贝 对象拥有复制特性,须实现NSCopying,NSMutableCopying协议,实现该协议的CopyWithZone:⽅法或MutableCopyWithZone:⽅法。
浅拷⻉贝实现
-(id)copyWithZone:(NSZone *)zone{
Person *person = [[[self Class]allocWithZone:zone]init];
p.name = _name;
p.age = _age;
return person;
}
深拷⻉贝的实现
-(void)copyWithZone:(NSZone *)zone{
Person *person = [[[self Class]allocWithZone:zone]init];
person.name = [_name copy];
person.age = [_age copy];
return person;
}
深浅拷⻉贝和retain之间的关系
copy、mutableCopy和retain之间的关系
Foundation中可复制的对象,当我们copy的是⼀个不可变的对象的时候,它的作⽤相当与retain(cocoa做的内存优化)
当我们使⽤mutableCopy的时候,⽆论源对象是否可变,副本是可变的
当我们copy的 是⼀个可变对象时,复本不可变

assign implies __unsafe_unretained ownership.
copy implies __strong ownership, as well as the usual behavior of copy semantics on the setter.
retain implies __strong ownership.
strong implies __strong ownership.
unsafe_unretained implies __unsafe_unretained ownership.
weak implies __weak ownership.

With the exception of weak, these modifiers are available in non-ARC modes.


15. 通知中心

同步操作
我在子线程发通知,接受者会在什么线程回调
main main : main
global main : global
main global : main
global global : global
不管消息接收者在什么线程addObserver,回调方法在发送消息线程执行()

16. NSTimer

1. 底层实现原理

mk_timer
GCD timer

2. 循环引用

WeakProxy
GCD timer

3.注意事项

现成问题,RunLoop问题

void dispatch_create_timer(id target, double timeInterval, void (^handler)(dispatch_source_t timer))
{
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_source_t timer =dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,0, 0, queue);

    dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), (uint64_t)(timeInterval *NSEC_PER_SEC), 0);

    __weak __typeof(target) weaktarget  = target;
    dispatch_source_set_event_handler(timer, ^{

        if (!weaktarget)  {
            dispatch_source_cancel(timer);
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                if (handler) handler(timer);
            });
        }
        
    });
    dispatch_resume(timer);
}

16. Tableview性能调优

1. 减少 CPU 负荷,较少计算

cell的高度缓存
减少重新布局
减少使用storyboard,xib

2. 减少主线程占用时间

异步加载图片
减少不必要的渲染

3. 预加载

在预加载方法中提前加载数据,
图片读取时,考虑到显示区域可能远小于图片大小,所以考虑使用
CGImageSourceCreateThumbnailAtIndex加载图片

你可能感兴趣的:(iOS 学习知识点杂项)