iOS面试之前准备的部分知识

/ 0评 / 0

OC对象的本质

  1. instance的isa指向class
  2. class的isa指向meta-class
  3. meta-class的isa指向基类的meta-class
  4. class的superclass指向父类的class,如果没有父类,superclass指针为nil
  5. meta-class的superclass指向父类的meta-class,基类的meta-class的superclass指向基类的class
  6. instance调用对象方法的轨迹,isa找到class,方法不存在,就通过superclass找父类
  7. class调用类方法的轨迹,isa找meta-class,方法不存在,就通过superclass找父类
  8. 类方法最终没有的话可能会调用对象方法😳

一些讲解

clang -rewrite-objc main.m -o main.cpp  //转成所有平台的c++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp  //转成手机平台的c++代码

object_getClass(string)  //获取类

object_getClass(obj)     //获取instance对象(实例对象)、class对象(类对象)、meta-class对象(元类对象)

子类实例对象调用父类方法过程:先通过实例对象的isa查找到类对象,通过类对象的superclass查找到父类的类对象(这里就能找到方法了),如果是类方法,要根据isa找到元类对象,再根据元类的对象的superclass查找到父类的元类对象

想窥探内存中类的结构只需要写一个新版object的结构体强转一下对象就可以看到了

KVO 键值监听 KVC 键值编码

自动生成的中间子类还会重写 class、dealloc、__isKOA这几个方法
class重写是因为用 objc_getClass() 获取的 和 直接调用class方法获取的类名打印出来并不一样,objc_getClass()可以直接看到系统插入的子类名称

KVC可以通过一个key来访问某个属性
常见的API有

-(void)setValue:(id)value forKeyPath:(NSString *)keyPath;
-(void)setValue:(id)value forKey:(NSString *)key;
-(id)valueForKeyPath:(NSString *)keyPath;
-(id)valueForKey:(NSString *)key;

setValue时候的步骤
getValue时候的步骤

Category 分类

一些讲解

分类的方法会在运行时合并到类中去,而且是插入到合并之前类方法的位置,这就是为什么同样的方法优先调用分类的
类扩展就是 .m文件中的interface,是在编译时就会合并到类中,而分类要到运行时才会合并

分类中可以写属性,但是只会生成set/get方法的声明,不会生成成员变量和set/get的实现,而且手动添加成员变量会报错

Block

一些讲解

block是个代码块,其实就是OC对象,封装了isa、函数地址、函数大小、外部的局部变量等
block本质结构体,并且内部有一个指向自己的指针

局部变量默认的修饰词是 auto,在函数执行完会释放,所以block中会直接捕获值,block后面再改变这个变量在block中保存的也不会变了,而使用static修饰的变量在block中捕获的是地址值,所以后面改变能影响到block中的打印

全局变量并没有捕获到block内部
self会被捕获所以self是个局部变量,因为每个方法其实默认会传递self和_cmd两个参数,
成员变量访问的时候其实是先拿到self再去访问成员变量😳

block有三种类型
block是oc对象,所以block也可以直接调用class方法,就可以查看类型

堆段    开发自己申请释放         Mallocblock(stackblock调用了copy就变成了这个)
栈段    系统分配               Stackblock(访问了auto变量的)
数据段                        GlobalBlock(没有访问auto变量的)

栈段的无法控制释放时间,所以会加一个copy放到堆上去,这就是为啥block成员变量要用copy

Malloc copy 引用计数加一
Stack  copy 变为malloc
global copy  什么也不做

以下情况ARC下将block会对Stack自动copy

static 修饰的变量可以直接在block中修改,那是因为前面说过这样的变量block中存的是地址指针

__block 修饰的变量是生成了一个新的结构体,结构体内部有一个指针forwarding指向自己,改值访问值都是通过这个指针,而block中捕获的变量就是转成这样一个成员变量

ARC解决循环引用
weak:
unsafe_unretained:不常用,不安全,使用完成之后不会吧对象置为nil,导致野指针
__block解决(必须要调用block,而且必须置空)

MRC解决循环引用(不支持weak)
unsafe_unretained:
__block:因为内部结构体对外部对象的引用总是弱引用

Runtime

@interface B : A
@end
@implementation B
- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"self------ %@", [self class]);
        NSLog(@"super------ %@", [super class]);   //其实消息接收者还是self,而class内部实现是objc_getClass(self),取决于self,所以返回的是自己
        NSLog(@"self------ %@", [self superclass]);
        NSLog(@"super------ %@", [super superclass]);  //其实消息接收者还是self
    }
    return self;
}
@end

2019-04-01 14:00:16.997350+0800 objc_msgSend[44557:6634030] self------ B
2019-04-01 14:00:16.997373+0800 objc_msgSend[44557:6634030] super------ B
2019-04-01 14:00:16.997403+0800 objc_msgSend[44557:6634030] self------ A
2019-04-01 14:00:16.997424+0800 objc_msgSend[44557:6634030] super------ A

NSLog(@"%d--------%d----------%d---------%d",
    [[NSObject class] isKindOfClass:[NSObject class]],
    [[NSObject class] isMemberOfClass:[NSObject class]],
    [[A class] isKindOfClass:[A class]],
    [[A class] isMemberOfClass:[A class]]
);

//等价于
[NSObject isKindOfClass:[NSObject class]],    //元类对类对象,循环往上层找,不管是那个类都返回yes
[NSObject isMemberOfClass:[NSObject class]],  //元类不等于类对象
[A isKindOfClass:[A class]],      //放元类才成功
[A isMemberOfClass:[A class]]     //放元类才成功
2019-04-01 14:00:16.997444+0800 objc_msgSend[44557:6634030] 1--------0----------0---------0

为什么会这样???

如果是用+号方法进行比较,那右边用的其实是元类
如果是用-号方法进行比较,那右边用的其实是类

@interface C : NSObject
@property (nonatomic, copy) NSString *name;
-(void)print;
@end

@implementation C
-(void)print {
    NSLog(@"--------%@", self.name);
}
@end

-(void)viewDidLoad {
    [super viewDidLoad];

    NSString *test = @"123";  //加个这打印的就是123(放后面打印的就不是这个啦,还是内存存储结构问题),不加打印的就是ViewController
    id cls = [C class];
    void *obj = &cls;
    [(__bridge id)obj print];
}

打印出来的是viewcontroller😳

一些讲解

isa指针
arm64之后 该指针变成了 isa_t 共用体
(了解一下结构)

位运算 一个字节可以存储很多信息
! 取反
| 按位或
& 按位与
~ 按位取反

位域 其实就是结构体的形式来表示,只是只申请需要位数的控件

struct {
    char rich : 1;
    char tall : 1;
    char hansome : 1;
} test;

共用体 共用一块内存

union {
    char bits;  //大于一个字节要换成别的类型
    struct {
        char rich : 1;
        char tall : 1;
        char hansome : 1;
    } test;
}

Class的结构(meta-class与class的结构是差不多哩)

struct objc_class {
    Class isa;
    Class superclass;
    cache_t cache;  //类信息缓存
    class_data_bits_t bits; //用于获取具体的类信息 -- &FAST_DATA_MASK --> struct class_rw_t {
    const class_ro_t *ro;  //存储只读信息的结构体,类名之类的
    method_list_t  methods;  //方法列表   (其实是个二维数组)
    property_list_t  properties; //属性列表   (其实是个二维数组)
    const protocol_list_t *protocols; //协议列表   (其实是个二维数组)
    }
}

开始是不存在rw_t的,类信息都存在ro_t中,在运行时会创建rw_t,将ro_t中的信息复制到rw_t中,因为rw_t是可读可写的,这样才能在运行时合并分类的信息

method_list_t里面存的method_t

struct method_t {
    SEL name;  //方法名
    const char * types;  //编码,包含了函数返回值、参数编码的字符串
    IMP imp;  //指向函数的指针
}

SEL 不管哪个类,不管写多少次,同样名称的 SEL 其实就是代表同样的东西,地址是一样的
比如 不同类的test方法 @selector(test) 地址完全一样,但是这只是代表名称

Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度

struct cache_t {
    struct bucket_t *_buckets;  //散列表
    mask_t _mask;   //散列表长度 - 1
    mask_t _occupied;  //已经缓存的方法数量
}

struct bucket_t {
    cache_key_t  _key;   //SEL作为key
    IMP _imp; //函数的内存地址
}

散列表原理(空间换时间)
@selector(方法名) & _mask 来查找方法的索引

查找顺序
先在cache中查找方法,查不到去bits中查,查不到去父类中去找,找到了会把父类中的方法缓存在自己的cache中

OC的方法调用:消息机制,给方法调用者发送消息 objc_msgSend

sel_registerName("test") 等价于 @selector(test)

objc_msgSend(对象, @selector(test));  //方法调用其实就是转成这了
消息接收者:对象
消息名称:test

//获取其他方法
class_getInstanceMethod()    //得到的Method其实就是method_t
//给类添加方法
class_addMethod();

消息机制三大阶段

消息发送---->动态方法解析---->消息转发

  1. 消息发送
    底层是汇编实现的 看arm64那个

  2. 动态方法解析
    找不到方法走下面的动态解析,会先判断是否动态解析过,解析过就不再解析了,直接走消息发送

+resolveInstanceMethod:
+resolveClassMethod:
  1. 消息转发
    还是找不到就走消息转发

    forwardingTargetForSelector
    methodSignatureForSelector

LLVM的中间代码

clang -emit-llvm -S main.m

能够生成.ll文件,这就是中间代码文件

Runtime会用到的API

1. 动态创建一个类(参数:父类,类名,额外的内存空间)
Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes)

2. 注册一个类(要在类注册之前添加成员变量)
void objc_registerClassPair(Class cls)

3. 销毁一个类
void objc_disposeClassPair(Class cls)

4. 获取isa指向的Class
Class object_getClass(id obj)

5. 设置isa指向的Class
Class object_setClass(id obj, Class cls)

6. 判断一个OC对象是否为Class
BOOL object_isClass(id obj)

7. 判断一个Class是否为元类
BOOL class_isMetaClass(Class cls)

8. 获取父类
Class class_getSuperclass(Class cls)

9. 获取一个实例变量信息
Ivar class_getInstanceVariable(Class cls, const char *name)

10. 拷贝实例变量列表(最后需要调用free释放)
Ivar *class_copyIvarList(Class cls, unsigned int *outCount)

11. 设置和获取成员变量的值
void object_setIvar(id obj, Ivar ivar, id value)
id object_getIvar(id obj, Ivar ivar)

12. 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types)

13. 获取成员变量的相关信息
const char *ivar_getName(Ivar v)
const char *ivar_getTypeEncoding(Ivar v)

14. 获取一个属性
objc_property_t class_getProperty(Class cls, const char *name)

15. 拷贝属性列表(最后需要调用free释放)
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

16. 动态添加属性
BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)

17. 动态替换属性
void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes,
unsigned int attributeCount)

18. 获取属性的一些信息
const char *property_getName(objc_property_t property)
const char *property_getAttributes(objc_property_t property)

19. 获得一个实例方法、类方法
Method class_getInstanceMethod(Class cls, SEL name)
Method class_getClassMethod(Class cls, SEL name)

20. 方法实现相关操作
IMP class_getMethodImplementation(Class cls, SEL name)
IMP method_setImplementation(Method m, IMP imp)
void method_exchangeImplementations(Method m1, Method m2)

21. 拷贝方法列表(最后需要调用free释放)
Method *class_copyMethodList(Class cls, unsigned int *outCount)

22. 动态添加方法
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

23. 动态替换方法
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)

24. 获取方法的相关信息(带有copy的需要调用free去释放)
SEL method_getName(Method m)
IMP method_getImplementation(Method m)
const char *method_getTypeEncoding(Method m)
unsigned int method_getNumberOfArguments(Method m)
char *method_copyReturnType(Method m)
char *method_copyArgumentType(Method m, unsigned int index)

26. 选择器相关
const char *sel_getName(SEL sel)
SEL sel_registerName(const char *str)

27. 用block作为方法实现
IMP imp_implementationWithBlock(id block)
id imp_getBlock(IMP anImp)
BOOL imp_removeBlock(IMP anImp)

交换类簇(NSString、NSArray、NSDictonary这些类,他们真是类型是别的类)的时候,记得不能直接用self来换,要用类似下面的方法

@implementation NSMutableArray(Extension)
+(void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{   //建议这样写,防止调用多次替换多次
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = classInstanceMethond(cls, @selector(insertObject:atIndex:));   //addObject走的也是这个方法
        Method method2 = classInstanceMethond(cls, @selector(dc_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
}

-(void)dc_insertObject:(id)object atIndex:(NSUInteger)index {
    if(object == nil) return;
    [self dc_insertObject:object atIndex:index];
}
@end

Runloop

//创建一个Observer,观察RunLoop的所有状态
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
/*
kCFRunLoopEntry = (1UL << 0), //即将进入Runloop 2^0 = 1
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理NSTimer 2^1 = 2
kCFRunLoopBeforeSources = (1UL << 2), //即将处理Sources 2^2 = 4
kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠  2^5 = 32
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒  2^6 = 64
kCFRunLoopExit = (1UL << 7), //即将退出runloop 2^7 = 128
*/

//这里打印出来的数字是上面数字X的2^X
NSLog(@"RunLoop状态  %zd", activity);
});

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

Runloop的休眠
runloop 使用mach_msg函数(内核层面的api,不开放)休眠线程,实现等待消息,有消息过来的时候再唤醒,节省资源

为啥要线程放在runloop中
不要每次都创建线程,减少性能消耗

多线程

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
    NSLog(@"111");
    [self performSelector:@selector(test) withObject:nil afterDelay:.0f];  //这一句没有打印,因为afterDelay用到了定时器,定时器是加到runloop中的,而子线程中默认没有runloop
    NSLog(@"333");
    //这里如果跑起来个runloop就可以看到test中的打印了
});

-(void)test {
    NSLog(@"222");
}

打印结果是:111、333
原因
performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器
子线程默认没有启动Runloop

NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"111");
    //只打印这个,除非这里加上runloop才会打印test中的值
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];

打印结果:111

使用队列组

//创建组
dispatch_group_t group = dispatch_group_create();

//创建队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 10; i ++) {
    NSLog(@"----任务一--%i=====%@-----",i,[NSThread currentThread]);
    }
});

dispatch_group_async(group, queue, ^{
    for (int i = 0; i < 5; i ++) {
    NSLog(@"----任务二--%i=====%@-----",i,[NSThread currentThread]);
    }
});

//唤醒
dispatch_group_notify(group, queue, ^{
    dispatch_async(dispatch_get_main_queue(), ^{
        for (int i = 0; i < 5; i ++) {
        NSLog(@"----任务三--%i=====%@-----",i,[NSThread currentThread]);
        }
    });
});

一些讲解

死锁:使用sync往当前串行队列添加任务,会卡主当前的串行队列(产生死锁)

  1. 死锁问题一
-(void)viewDidLoad {
[super viewDidLoad];

NSLog(@"任务一");

//这里会造成线程死锁,这是由于队列的原因,FIFO,排队执行,主队列里执行同步操作
dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"任务二");
    });

    NSLog(@"任务三");
}
  1. 死锁问题二
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_SERIAL); //串行
    dispatch_queue_t queue2 = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT); //并发
    dispatch_async(queue, ^{
    NSLog(@"任务一");

    dispatch_sync(queue, ^{
    NSLog(@"任务二");
    });

    NSLog(@"任务三");
});

上面的也会死锁,两个block互相等待,原理同上个问题

多线程的安全隐患

  1. 资源共享
    1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    比如多个线程访问同一个对象、同一个变量、同一个文件

买票问题、银行存取钱问题

  1. 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

  2. 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
    常见的线程同步技术是:加锁
    OSSpinLock:
    叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
    目前已经不再安全,可能会出现优先级反转问题
    如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁

//初始化
OSSpinLock ticketLock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&ticketLock);
/*做操作*/
//解锁
OSSpinLockUnlock(&ticketLock);

os_unfair_lock: //从汇编层面看也是互斥锁
os_unfair_lock用于取代不安全的OSSpinLock ,从iOS10开始才支持
从底层调用看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等

//初始化
os_unfair_lock ticketLock = OS_UNFAIR_LOCK_INIT
//加锁
os_unfair_lock_lock(&ticketLock);
/*做操作*/
//解锁
os_unfair_lock_unlock(&ticketLock);

pthread_mutex:
mutex叫做”互斥锁”,等待锁的线程会处于休眠状态

//初始化锁属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); //互斥锁
//pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE); //递归锁   允许同一个线程对一把锁进行重复加锁
//初始化
pthread_mutex_t ticketLock;
pthread_mutex_init(&ticketLock, &attr);
//释放
pthread_mutexattr_destroy(&attr);

//加锁
pthread_mutex_lock(&ticketLock);
/*做操作*/
//解锁
pthread_mutex_unlock(&ticketLock);
//释放
pthread_mutex_destroy(&_ticketLock);

条件的使用
//初始化
pthread_mutex_t ticketLock;
pthread_mutex_init(&ticketLock, NULL); //第二个参数也可以传NULL,采用默认属性
//条件
//初始化条件
pthread_cond_t condition;
pthread_cond_init(&condition, NULL);
//等待条件(进入休眠,放开mutex锁;被唤醒后再次对mutex加锁)
pthread_cond_wait(&condition, &ticketLock);
//激活一个等待该条件的线程
pthread_cond_signal(&condition);
//激活所有等待该条件的线程
pthread_cond_broadcast(&condition);
pthread_cond_destroy(&condition);

dispatch_semaphore
semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_queue(DISPATCH_QUEUE_SERIAL)
直接使用GCD的串行队列,也是可以实现线程同步的

NSLock
对pthread_mutex普通锁的封装

NSRecursiveLock
对pthread_mutex递归锁的封装

NSCondition
对pthread_mutex与事件cond的封装

NSConditionLock
对NSCondition的进一步封装

@synchronized
@synchronized是对mutex递归锁的封装
源码查看:objc4中的objc-sync.mm文件
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
利用传进去的对象作为key,存储结构为hashtable

性能从高到低排序

os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized

atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
可以参考源码objc4的objc-accessors.mm
它并不能保证使用属性的过程是线程安全的
太耗性能,所以iOS一般不用,mac上用的多点

  1. 上面的场景就是典型的“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有
    pthread_rwlock:读写锁
    dispatch_barrier_async:异步栅栏调用(写的时候用这个,保证同一时间只有一个写操作)

内存管理

一些讲解

  1. CADisplayLink 与 NSTimer 的循环引用解决方法
    NSProxy 专门用来消息转发的,所以大部分的方法都会转发,比如 isKindOfClass:

  2. GCD 定时器
    与系统内核挂钩,所以时间准确,并且与runloop无关

dispatch_queue_t queue = dispatch_get_main_queue();
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);  //要用强引用保留
dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 2.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(self.timer, ^{
    NSLog(@"1");
});
dispatch_resume(self.timer);
内存布局
  1. 代码段:编译之后的代码
  2. 数据段
    字符串常量:比如NSString *str = @"123"
    已初始化数据:已初始化的全局变量、静态变量等
    未初始化数据:未初始化的全局变量、静态变量等
  3. 栈:函数调用开销,比如局部变量。分配的内存空间地址越来越小
  4. 堆:通过alloc、malloc、calloc等动态分配的空间,分配的内存空间地址越来越大

Tagged Pointer
从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储

在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值

使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中

当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

objc_msgSend能识别Tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省了以前的调用开销

如何判断一个指针是否为Tagged Pointer?
iOS平台,最高有效位是1(第64bit)
Mac平台,最低有效位是1

思考以下2段代码能发生什么事?有什么区别?

dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 2000; i ++) {
    dispatch_async(queue, ^{
        self.name = [NSString stringWithFormat:@"abcdefghijk"];   //会多次释放,坏内存访问,j这句加锁就行
        self.name = [NSString stringWithFormat:@"abc"]; //这就没问题,因为Tagged Pointer
    });
}

//- (void)setName:(NSString *)name {
//    if (_name != name) {
//        [_name release];
//        _name = name;
//    }
//}
OC对象的内存管理

在iOS中,使用引用计数来管理OC对象的内存

一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间

调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

内存管理的经验总结
当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1,而类方法创建的比如array等类方法不用释放,因为内部做了autorelease

copy总结
产生一个副本对象,跟源对象互不影响

NSString *test = [NSString stringWithFormat:@"123啊发发发"];
NSString *test1 = [test copy]; //浅拷贝
NSMutableString *test2 = [test mutableCopy]; //深拷贝

NSMutableString *test3 = [[NSMutableString alloc] initWithString:@"123大发发"];
NSString *test4 = [test3 copy]; //深拷贝
NSMutableString *test5 = [test3 mutableCopy]; //深拷贝

如果字符串太短就会使用Tagged Pointer技术,那就不是真正意义上的对象,就没有引用计数这一说

属性关键词:不可变属性可以用copy,可变属性不能用copy要用strong,因为set方法中赋值会使用copy而不是retain变成了不可变属性,改变的时候会报错,而字符串可不可变都用copy因为字符串一般只是赋值操作,并不需要append

@property(nonatomic, copy) NSString *name;

-(void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}

自定义类想使用copy需要实现NSCopying协议,实现copyWithZone方法

引用计数是存储在isa中的,但是只有19位,如果存不下会存在SideTable结构体中的refcnts散列表中

weak指针的原理
当一个对象要释放时,会自动调用dealloc,接下的调用轨迹是

dealloc
_objc_rootDealloc
rootDealloc
object_dispose
objc_destructInstance、free

如果是weak指针会存储在SideTable结构体中的weak_table散列表中

autorelease

@autoreleasepool {

}

编译之后如下

objc_autoreleasePoolPush();  //构造函数中的方法
//中间是代码
objc_autoreleasePoolPop(atautoreleasepoolobj);  //析构函数中的方法

每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
id *next指向了下一个能存放autorelease对象地址的区域

可以通过以下私有函数来查看自动释放池的情况
extern void _objc_autoreleasePoolPrint(void);

iOS在主线程的Runloop中注册了2个Observer
第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
第2个Observer
监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()
监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

性能优化

一些讲解

  1. 离屏渲染消耗性能的原因
    需要创建新的缓冲区
    离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕

  2. 哪些操作会触发离屏渲染?
    光栅化,layer.shouldRasterize = YES
    遮罩,layer.mask
    圆角,同时设置layer.masksToBounds = YES、layer.cornerRadius大于0
    考虑通过CoreGraphics绘制裁剪圆角,或者叫美工提供圆角图片
    阴影,layer.shadowXXX
    如果设置了layer.shadowPath就不会产生离屏渲染

架构设计

  1. MVC-变种
    Model-Controller-View 不同点在于View与Model之间添加一层联系
    优点:Controller瘦身,View内部封装
    缺点:View依赖于Model

  2. MVP
    Model-Presenter-View 业务逻辑放在Presenter中
    优点:
    缺点:

  3. MVVM
    Model-ViewModel-View 与MVP不同的点是属性绑定监听,ViewModel管理数据与视图,View监听ViewModel中数据的变化并跟着变化
    优点:
    缺点:

  4. 三层架构、四层架构
    界面层、业务层、数据层
    界面层、业务层、网络层、数据层
    MVC/MVP/MVVM都是界面层的东西

  1. 设计模式可以分为三大类
    1. 创建型模式:对象实例化的模式,用于解耦对象的实例化过程
      单例模式、工厂方法模式,等等
    2. 结构型模式:把类或对象结合在一起形成一个更大的结构
      代理模式、适配器模式、组合模式、装饰模式,等等
    3. 行为型模式:类或对象之间如何交互,及划分责任和算法
      观察者模式、命令模式、责任链模式,等等

加密与签名

1 . base64编码

  1. 对称密码算法
    加密解密用的同一个密钥
    DES,3DES,AES(高级密码标准,钥匙串用的就是这个)
    3DES:进行3次DES加密

  2. 加密方式
    ECB:点子代码本,每一个数据块进行独立加密
    CBC:密码块链,使用一个密码和一个初始化向量对数据进行加密,数据块相互依赖,丢失一个数据块就不能解密,防范窃听

  3. 非对称密码算法
    RSA算法
    公钥加密,私钥解密
    私钥加密,公钥解密

  4. Hash散列算法
    MD4、MD5、SHA-1、SHA-2、SHA-3

  5. 方案HMAC(不是算法)
    特点:算法公开,同一个数据加密算出的结果是相同的,不同数据加密之后是定长的,不能反算(信息的摘要,信息指纹,做数据的识别)
    这些因为算出的结果相同,所以不太安全,以前的解决办法是加盐(MD5+salt这个盐就是一个固定的字符串,但是也有泄漏的风险)
    方案HMAC可以解决加盐也不安全的问题
    使用key(服务器随机生成,保存到数据库的,一个账号一个key)进行明文加密,然后进行两次Hash算法

如有错误,敬请指正~

评论已关闭。