面试亲历一些题 | 一

一,此代码在主线程调用有何不妥:

- (void)foo {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(printSomething) userInfo:nil repeats:YES];
    [timer fire];
}
- (void)printSomething {
    static NSInteger count = 0;
    count ++;
    NSLog(@"timer count: %ld   - - - %@",count,[NSThread currentThread].description);
}

不妥,计时器造成了循环引用,因为计时器内部对target没有进行弱引用处理,所以会造成循环引用,处理办法:1,要么用block类型创建计时器;2,用第三方对象proxy作为target,拦截selector后让self进行处理,不会造成循环引用。

其余不妥,重点:
1,static关键字修饰局部变量,只会初始化一次;
2,static关键字修饰局部变量,在程序中只有一份内存;
3,static修饰的局部变量生命周期为,运行到当前代码块为开始,程序结束才销毁。
所以,当前count变量正常累加,在初始化一次之后,不会再去初始化。

//
//所以输出的是:
timer count: 1   - - - <NSThread: 0x6000038f4900>{number = 1, name = main}
2020-11-10 23:42:58.528381+0800 MianShiDemo[75471:4483232] 
0x107dfb890
2020-11-10 23:43:01.529220+0800 MianShiDemo[75471:4483232] 
timer count: 2   - - - <NSThread: 0x6000038f4900>{number = 1, name = main}
2020-11-10 23:43:01.529440+0800 MianShiDemo[75471:4483232] 
0x107dfb890
2020-11-10 23:43:04.529535+0800 MianShiDemo[75471:4483232] 
timer count: 3   - - - <NSThread: 0x6000038f4900>{number = 1, name = main}
2020-11-10 23:43:04.529851+0800 MianShiDemo[75471:4483232] 
0x107dfb890

如果是异步线程中,应该如何处理:

- (void)foo {
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(printSomething) userInfo:nil repeats:YES];
    [timer fire];
    //必须开启当前线程的Runloop
    [[NSRunLoop currentRunLoop] run];
}

- (void)printSomething {
    static NSInteger count = 0;
    count ++;
    NSLog(@"\ntimer count: %ld   - - - %@",count,[NSThread currentThread].description);
    NSLog(@"\n%p",&count);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    //开启子线程调用NSTimer
    [NSThread detachNewThreadWithBlock:^{
        [self foo];
    }];

延展:
1,全局变量的作用域是整个项目文件,例如此全局变量:

#import "ViewController.h"
#import <objc/message.h>

NSInteger var = 100;

@interface ViewController ()

@end

@implementation ViewController

2,如果要在其他文件中获取此全局变量,那么在变量前增加extern修饰,例如:

@implementation ViewController_1

- (void)viewDidLoad {
    [super viewDidLoad];
    extern NSInteger var;
    NSLog(@"%ld",var);
    // Do any additional setup after loading the view.
}
//输出
2020-11-10 23:54:09.214375+0800 xxxxDemo[75617:4494813] 100

3,如果不想让外部变量进行访问,可在定义的时候,前边加static。

二,缓存和内存中数据的一致性;

内存中数据,指的就是内存中的缓存数据,程序要读取一个数据,那么它从磁盘中将数据放在寄存器里边,然后通过寄存器加载到内存中再进行读取,这样对于一些高频使用的数据来说,时常去读取磁盘,程序的效率没有内存缓存去读取的高,而且会对磁盘寿命造成损耗。
所以为何要写内存缓存数据:
优点:主要是为提高执行效率。
缺点:增加了资源成本,也就是内存会增大。其实就是拿空间去换时间的一个操作。

objective-c里边的的内存缓存用NSCache去实现,第三方还有很多缓存框架。
内存缓存在实际场景中保存什么:
1,基础配置的数据,基本上不改动的数据;
2,或者高频率读取等。

如何保证内存中的磁盘中缓存数据一致:
读:先读缓存,如缓存没有,就读数据库(磁盘数据),然后取出数据后放入缓存。
写:先更新数据库,然后再删除缓存。
PS,如果数据有改动,就要删除内存中的数据,然后更新磁盘中的数据。

三,模拟通知;

暂无

四,线上奔溃如何处理,如何将崩溃日志传递给后台;

1,自身实现:

void uncaughtExceptionHandler(NSException *exception)
{
    // 异常的堆栈信息
    NSArray *stackArray = [exception callStackSymbols];
    // 出现异常的原因
    NSString *reason = [exception reason];
    // 异常名称
    NSString *name = [exception name];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    NSLog(@"%@", exceptionInfo);
    
    NSMutableArray *tmpArr = [NSMutableArray arrayWithArray:stackArray];
    [tmpArr insertObject:reason atIndex:0];
    
    //保存到本地  --  当然你可以在下次启动的时候,上传这个log
    [exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/error.log",NSHomeDirectory()]  atomically:YES encoding:NSUTF8StringEncoding error:nil];
}

2,第三方,例如友盟,bugly。

五,两个子线程,有什么方法可以让其串联执行;

1,信号量,等待第一个执行完之后再执行第二个;

    dispatch_queue_t userQue_1 = dispatch_queue_create("userQueue.com", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t userQue_2 = dispatch_queue_create("userQueue.com", DISPATCH_QUEUE_CONCURRENT);
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    dispatch_async(userQue_1, ^{
        //先执行此任务
        NSLog(@"userQue_1%@",[NSThread currentThread].description);
        sleep(2);
        NSLog(@"1执行完");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(userQue_2, ^{
        //得到发送的信号后,再去执行等待的任务,也就是信号量大于0的时候
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"userQue_2%@",[NSThread currentThread].description);
    });

2,栅栏函数,前提是如果两个子线程在同一队列中,可将两子线程进行分割;
3,pthread互斥锁,pthread_mutex_t,如下:

    __block pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
    [NSThread detachNewThreadWithBlock:^{
        pthread_mutex_lock(&mutex);
        NSLog(@"%@,线程1 \n",[NSThread currentThread].description);
        pthread_mutex_unlock(&mutex);
    }];
    [NSThread detachNewThreadWithBlock:^{
        pthread_mutex_lock(&mutex);
        NSLog(@"%@,线程2 \n",[NSThread currentThread].description);
        pthread_mutex_unlock(&mutex);
    }];
    [NSThread detachNewThreadWithBlock:^{
        pthread_mutex_lock(&mutex);
        NSLog(@"%@,线程3 \n",[NSThread currentThread].description);
        pthread_mutex_unlock(&mutex);
    }];
    [NSThread detachNewThreadWithBlock:^{
        pthread_mutex_lock(&mutex);
        NSLog(@"%@,线程4 \n",[NSThread currentThread].description);
        pthread_mutex_unlock(&mutex);
    }];
//输出
2020-11-19 19:51:43.535783+0800 xxxxxDemo[33897:1965863] <NSThread: 0x600001afc880>{number = 8, name = (null)},线程1 
2020-11-19 19:51:43.536004+0800 xxxxxDemo[33897:1965864] <NSThread: 0x600001afe880>{number = 9, name = (null)},线程2 
2020-11-19 19:51:43.536189+0800 xxxxxDemo[33897:1965865] <NSThread: 0x600001afa740>{number = 10, name = (null)},线程3 
2020-11-19 19:51:43.536345+0800 xxxxxDemo[33897:1965866] <NSThread: 0x600001abb340>{number = 11, name = (null)},线程4 

六,Mysql索引,事务。

索引:
索引是帮助数据库高效获取数据的数据结构。
表现形式:
PRIMARY KEY 主键
UNIQUE 唯一键
INDEX 普通索引

事务:
事务指逻辑上的一组操作,组成这组操作的各个单元,此操作单元具有原子性,要不全部成功,要不全部不成功。
个人理解:事务是一个存储SQL事件有所约束的队列,也就是执行多条SQL的一组逻辑操作。
BEGIN TRANSACTION //事务开始
SQL1
SQL2
COMMIT/ROLLBACK //事务提交或回滚

七,如果一个for循环里边有子线程去操作一个可变数组(对可变数组进行增加元素操作),那么当前代码环境会发生什么,为什么会发生?

1,多个线程去操纵一个数组,非原子性,会导致数据不完整,有冲突,用栅栏函数进行加锁;
2,因为多个任务去操作一个内存地址,可能会导致数组越界,导致崩溃。

八,什么是元类?

元类,是一个类对象的类,也就是当前类对象结构体中isa指向的类。
例如per对象对应的类是Person类,那么Person类也是一个类对象,因为它可以调用类方法,从而这个Person类对象的obj_class中isa指向的类称为元类。
所有的类自身也是一个对象,我们可以向这个对象发送消息(即调用类方法)。 既然是对象,那么它也是一个objc_class结构体指针,它包含一个指向其类的一个isa指针。当前isa指针指向了meta-class,meta-class的结构体objc_class里边包含了其自身的类方法,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
例如输出元类:
objc_getMetaClass(const char * _Nonnull name);

NSLog(@"%@",[[^{
        NSLog(@"this is block 3 - - -");
} superclass]superclass]);
//打印block的元类地址
void (^block_1)(void) = ^{
    NSLog(@"this is block 1");
};
const char *name = object_getClassName(block_1);
NSLog(@"%p",objc_getMetaClass(name));

九,block为什么是一个对象?

block对象本身是由objc_class组成,并且objc_class结构体拥有指针isa指向其类对象,block的结构体为:

struct objc_class : objc_object {
    //class isa,这里是继承与objc_object的,所以有isa指针
    Class superclass; // 指向父类的地址
    cache_t cache;   // 方法缓存列表
    //结构体,内部存储着主要的数据,通过&不同的掩码值获取不同的信息地址
    class_data_bits_t bits;  
    // 获取存放在bits中的可读可写的数据信息
    class_rw_t *data() { 
        return bits.data();
    }
   ......
}
//因为objc_class结构体继承与objc_object,所以里边有指向class对象的指针isa,

在Objective-C中,任何以一个指向 Class 结构体的指针isa开始的结构体都可以视为一个 objc_object,也就是对象。

isa是一个Class 类型的指针,每个实例对象有个isa的指针,它指向对象的类。每个对象都有一个类,对象的类是isa指针决定的,即 isa 指针指向对象所属的类(也可以立即成指向当前block对象的内存地址),block结构中有isa指向class,Class里有isa的指针,它指向对象的元类,所以它是一个对象,此对象的元类是NSObject。

十,block有几种?

    //全局block
    void (^block_1)(void) = ^{
        NSLog(@"this is block 1");
    };
    //栈block,
    __block NSInteger num = 10;
    void (^block_2)(void) = ^{
        NSLog(@"this is block 2 - - - %ld",num);
    };
    NSLog(@"%@\n%@\n%@\n",[block_1 class],[block_2 class],[^{
        NSLog(@"this is block 3 - - -%ld , 这里是栈",num);
    } class]);

如果block作为函数的参数,它保存在内存中的栈区;

//输出
2020-11-11 16:39:51.570257+0800 xxxxDemo[77500:4699292] 
__NSGlobalBlock__  //第一个没有引用外部变量,属于全局block。
__NSMallocBlock__  //第二个没有进行copy应该是栈block,但输出的是堆block,因为环境是ARC,系统自动将栈block转换成了堆block(只要当前block一旦赋值,就会触发copy),便于调用。(给第二个block赋值了变量,也就是进行了copy,所以在堆里)
__NSStackBlock__   //第三个是栈block,它的作用域是block执行结束,所以他的出了作用域会自动销毁,如果是MRC环境,需要对此类block进行copy操作。

三种:全局数据区,堆,栈。
1,不使用外部变量的block是全局block,对全局block进行copy,仍是全局block,程序结束后由系统释放;
2,使用外部变量并且未进行copy操作的block是栈block,block块运行结束后系统自动释放;
3,对栈Block进行copy,将会copy到堆区,变为堆block,对堆Block进行copy,将会增加引用计数,需手动释放。

十一,block是否可以修改外部变量(这里的变量是普通非对象的变量)?

首先,创建一个局部变量,例如NSInteger num = 10; 它存储在栈区,如果当前定义是一个全局变量,那么他存放在静态区,也就是全局区,如果是staic修饰的变量,也存在(全局区)静态区。

//修改示例
NSInteger val = 4;
void (^block)(void) = ^{
     val++;
     NSLog(@"val by value - - - %ld",val);
};
block();
//此代码是不能够修改外部变量val,并且会报错
Variable is not assignable (missing __block type specifier)
//程序告诉我们,丢失了变量修饰符__block。

为何不能修改:首先ARC下的创建的block,如果没有引用外部变量,一旦给block赋值变量,系统会自动将它处理成堆block(系统自动视为copy),也就是说当前block存在于堆中。
那么当前外部传进来的变量,只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。
block捕获的外部变量可以改变其值的是:静态变量,静态全局变量,全局变量,因为他们存在于全局区,初始化后只有一份内存地址。
当前捕获的外部变量,block捕获了其变量的值,并且保存在block结构体中的成员变量里边,其内存地址跟block内存地址相等。
那么怎么样才能够修改其外部变量的值呢?

给局部变量(也就是所谓的自动变量)增加__block修饰,增加__block的原理:
将__block修饰的变量,如果当前的block块是在堆中,那么__block变量就会copy到堆上,然后将__block转化成一个结构体,此结构体为:
1,isa指针;
2,指向自身类型的__forwarding指针;
3,一个标记flag;
4,它的大小:
5,变量值。
那么当__block变量被copy到堆上,那么当前堆上的block块就持有了此堆上的变量,此变量结构体中有指针,所以可以修改其值。

如果是对象类型的局部变量,那么不需要加__block也可以进行操作,因为其传递的是对象指针,当传递过来的对象,block也会在堆里边重新拷贝一份,操作新拷贝的局部变量,例如:

    NSMutableString *mstr = [NSMutableString stringWithString:@"hi"];
    void (^block)(void) = ^{
        [mstr appendString:@" objective-c!"];
        NSLog(@"%@ - - -%p",mstr,&mstr);
    };
    NSLog(@"%@ - - -%p",mstr,&mstr);
    block();
    NSLog(@"%@ - - -%p",mstr,&mstr);
    //输出,可以看出block执行完之后mstr的地址和block块里边的地址是相同的。
2020-11-11 23:55:10.216056+0800 xxxxDemo[80069:5007352] hi - - -0x7ffee7e73128
2020-11-11 23:55:10.216243+0800 xxxxDemo[80069:5007352] hi objective-c! - - -0x600000a11b50
2020-11-11 23:55:10.216360+0800 xxxxDemo[80069:5007352] hi objective-c! - - -0x7ffee7e73128
//如果加了__block输出也是如此,因为加不加__block,只要改对象在block中引用了,都会在堆里边重新copy一份。
//=======
    __block NSMutableString *mstr = [NSMutableString stringWithString:@"hi"];
    void (^block)(void) = ^{
        [mstr appendString:@" objective-c!"];
        NSLog(@"%@ - - -%p",mstr,&mstr);
    };
    NSLog(@"%@ - - -%p",mstr,&mstr);
    block();
    NSLog(@"%@ - - -%p",mstr,&mstr);
    //输出
2020-11-11 23:58:15.981031+0800 xxxxDemo[80093:5009922] hi - - -0x600003024478
2020-11-11 23:58:15.981191+0800 xxxxDemo[80093:5009922] hi objective-c! - - -0x600003024478
2020-11-11 23:58:15.981352+0800 xxxxDemo[80093:5009922] hi objective-c! - - -0x600003024478

延展:

    NSInteger val = 4;
    void (^block)(void) = ^{
        NSLog(@"%ld - - - %p\n",val,&val);
    };
    val++;
    NSLog(@"%ld - - - %p\n",val,&val);
    block();
    NSLog(@"%ld - - - %p\n",val,&val);
//输出
2020-11-12 00:02:26.268682+0800 xxxxDemo[80160:5015099] 5 - - - 0x7ffeea98f128
2020-11-12 00:02:26.268839+0800 xxxxDemo[80160:5015099] 4 - - - 0x6000023980b0
2020-11-12 00:02:26.268954+0800 xxxxDemo[80160:5015099] 5 - - - 0x7ffeea98f128

这里就比较好理解,在外部++了,但是为什么block块内部输出的还是4,因为block只是捕获了val(自动变量)的值,并非内存地址,并且将值保存在了block结构体中的成员变量里边,所以Block内部输出的是block之前获取的那个值,也就是自身结构体中的那个值。

十二,objc_class包含什么?

objc_class是指向class的结构体类型。objective_c中,类是由Class类型来表示的。
里边包含了:

struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE; // 父类
     const char *name                         OBJC2_UNAVAILABLE; // 类名
     long version                             OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
     long info                                OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
     long instance_size                       OBJC2_UNAVAILABLE; // 该类的实例变量大小
     struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE; // 该类的成员变量链表
     struct objc_method_list *methodLists     OBJC2_UNAVAILABLE; // 方法定义的链表
     struct objc_cache *cache                 OBJC2_UNAVAILABLE; // 方法缓存
     struct objc_protocol_list *protocols     OBJC2_UNAVAILABLE; // 协议链表
#endif
}OBJC2_UNAVAILABLE;

十三,以下代码输出顺序是什么?

- (void)printTest
{
    NSLog(@"输出 - - - 2\n");
}
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t gQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(gQueue, ^{
        NSLog(@"输出 - - - 1\n");
        [self performSelector:@selector(printTest) withObject:nil afterDelay:0.0];
        NSLog(@"输出 - - - 3\n");
    });
 }

输出1和3,2不去输出,因为performSelector:withObject:nil:afterDelay底层在定义的时候系统自动创建一个NSTimer,然后去执行selector,因为当前为子线程,子线程的Runloop得手动去开启,所以不会去执行2。正确的做法为:

- (void)printTest
{
    NSLog(@"输出 - - - 2\n");
}
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t gQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(gQueue, ^{
        NSLog(@"输出 - - - 1\n");
        [self performSelector:@selector(printTest) withObject:nil afterDelay:0.0];
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"输出 - - - 3\n");
    });
}
//控制台输出
2020-11-11 21:53:50.134437+0800 xxxxDemo[79188:4905794] 输出 - - - 1
2020-11-11 21:53:50.134733+0800 xxxxDemo[79188:4905794] 输出 - - - 2
2020-11-11 21:53:50.134889+0800 xxxxDemo[79188:4905794] 输出 - - - 3

如果是换成performSelector:withObject方法,而没有afterDelay参数,在子线程中的执行顺序如何?

答:直接输出1,2,3,因为performSelector:withObject方法没有创建NSTimer,所以不需要开启Runloop,所以performSelector:withObject的事件直接会在子线程中执行。

//输出
2020-11-11 21:58:59.650205+0800 xxxxDemo[79237:4910349] 输出 - - - 1
2020-11-11 21:58:59.650367+0800 xxxxDemo[79237:4910349] 输出 - - - 2
2020-11-11 21:58:59.650506+0800 xxxxDemo[79237:4910349] 输出 - - - 3

十四:App从启动到结束都做了什么?

1,进入main函数;
1,创建UIApplication创建主线程,其包括RunLoop(一一对应);
2,对象加载配置文件,info.plist,确定沙盒路径,创建沙盒;
3,加载动态库;
5,加载+load方法和category的扩展;
6,监听系统事件,处理等一系列操作;
7,程序结束。

十五,KVC的原理;

//应用
KVCClass *kvcObj = [KVCClass new];
kvcObj.kvModel = [KVCClass new];
[kvcObj setValue:@"this is name value" forKey:@"name"];
NSLog(@"%@",[kvcObj valueForKey:@"name"]);
[kvcObj setValue:@"this is name value by kvModel" forKeyPath:@"kvModel.name"];
NSLog(@"description - - - %@",[kvcObj valueForKeyPath:@"kvModel.name"]);
//被应用的类里边
-(void)setValue:(id)value forUndefinedKey:(NSString *)key;
//保证kvc读取不到对应键的值,返回的内容
-(id)valueForUndefinedKey:(NSString *)key;
//设置空值时
- (void)setNilValueForKey:(NSString *)key;

所谓KVC就是用key,value的形式去访问类属性,更深层一点就是用keyPath访问内部属性对象的属性。

实现原理:
KVC的赋值本质上只是调用了属性的setter方法,setter方法会按照setVariable、_setVariable、setIsVariable的优先级进行调用,还没有,则按_ariable、_isVariable、variable、isVariable查找成员变量。
取值调用了属性的getter方法,按照方法的优先级进行搜寻,然后再搜寻成员变量。
赋值:
1,搜索是否有setVariable(也就是属性的set方法)方法,Variable是成员变量名,如果没有则会搜索setIsVariable方法;
2,如果没有setIsVariable方法,则会查看当前类中的+ (BOOL)accessInstanceVariablesDirectly;方法是否返回YES,当前方法是定义是否去搜索访问当前类的成员变量。如果返回NO,则抛出异常,进入valueForUndefinedKey方法中。
3,如果返回返回YES,按 _variable、_isVariable、variable、isVariable的顺序搜索成员变量,直到返回。
4,如果都没有此些方法和成员变量,直接抛出异常,进入valueForUndefinedKey方法。
取值:
1.按先后顺序搜索getVar、var、isVar、_getVar、_var五个方法,按顺序哪个方法被实现,返回值就是改方法的返回值,后面的方法不再运行。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
2.若这五个方法都没有找到,则会调用+ (BOOL)accessInstanceVariablesDirectly方法判断是否允许取成员变量的值。
若返回NO,直接调用- (nullable id)valueForUndefinedKey:(NSString *)key方法,默认是奔溃。
若返回YES,会按先后顺序取_var、_isVar、 var、isVar成员变量的值。
3.返回YES时,_var、_isVar、 var、isVar的值都没取到,调用- (nullable id)valueForUndefinedKey:(NSString *)key方法。

实用场景:
1,给属性/成员变量赋值;
2,字段典转模型(可解析json);
3,修改系统控件的内部属性,例如: [pageControl setValue:[ UIImageimageNamed: @”xx.png”] forKeyPath: @”_pageImage”]; 

十六,通知,

通知,哪个线程发送通知,那么接受的通知事件就在哪个线程中执行,例如:

        [NSThread detachNewThreadWithBlock:^{
            NSLog(@"%@",[NSThread currentThread].description);
            [[NSNotificationCenter defaultCenter] postNotificationName:@"notiTest" object:nil];
        }];
//
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notiOperate) name:@"notiTest" object:nil];
- (void)notiOperate
{
    NSLog(@"%@",[NSThread currentThread].description);
}
//输出
2020-11-15 18:45:51.593169+0800 xxxxDemo[15338:747324] <NSThread: 0x600003454f80>{number = 7, name = (null)}
2020-11-15 18:45:51.593456+0800 xxxxDemo[15338:747324] <NSThread: 0x600003454f80>{number = 7, name = (null)}

十六,如何实现KVO?

使用:
//注册观察者
– (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
//监听事件
– (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
//移除监听
– (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(nullable void *)context API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
苹果官方推荐的方式是,在init的时候进行addObserver,在dealloc时removeObserver,这样可以保证add和remove是成对出现的,否则会崩溃。

十七,数据存储的有几种形式?

内存:栈,堆,全局区,常量区,代码区。
磁盘:Userdefault,Sqlite,Data(二进制),钥匙串。

十八,UIImage优化;
http://www.iashes.com/2020/11/13-405.html

十九,socket断点续传;

二十,常用的LLDB命令有哪些?

1,p命令,查看对象的指针地址;
2,po,查看对象的description;
3,expression,动态调试,执行某个赋值表达式;
4,call,动态调用某个函数;

5,bt,打印出当前线程的堆栈信息。

二十一,isKindOfClass和isMemberOfClass的区别。

– (BOOL) isKindOfClass: classObj判断当前对象是否是当前类或者当前类的子类的实例。
– (BOOL) isMemberOfClass: classObj 判断当前对象是否是当前类的实例。

二十二,OC如何寻找类方法和实例方法?

//类方法寻找
Student -> Member -> NSObject(继承关系)
Student :   + create;
[Student  create]; 
1,元类的类方法列表查询;
2,在去当前对象指向的类对象的方法列表中查询Member - > Student。
//实例方法寻找
Person - > SuperClass -> S_SuperClass(继承关系)
实例方法寻找:
1,寻找Person类里是否有(person类结构体中的缓存列表),如果有直接执行,如果没有,则向下;
2,再寻找SuperClass(类结构体中的缓存列表),同上;
3,再寻S_SuperClass(类结构体中的缓存列表),同上;
直到根类。

二十三,对象是如何去查找方法的,如果方法没有定义会如何处理?

首先,每一个对象是一个结构体,此结构体中有指向objc_class类的指针isa,也就是元类的地址,那么当前结构体中有一个元素,cache_t cache,是用来缓存最近调用过的方法。
那么当对象调用某一个实例方法的时候:
1,系统会按照方法名去cache_t中查找方法,找到了则直接调用;
2,如果未找到,进入类对象方法列表中查找(也就是isa所指向的对象中查找),此列表包含了分类的方法。
3,如果在类对象方法列中找到了此方法,先将方法加入cache_t中,然后调用方法。
4,如在方法列表中未找到,则通过superclass找到父类,在父类中进行1、2、3步骤。(父类的cache,父类类对象方法中,然后写入缓存);
5,如果最终未找到方法,会进入方法的动态解析阶段。

方法的静态解析阶段,如果没有找到方法,那么:
1,首先将调用其本类的resolveInstanceMethod:方法,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理该 unknown selector。在继续往下执行转发机制之前,本类有机会新增一个处理此 selector 的方法。所以resolveInstanceMethod:的使用:

1,首先会对当前实例方法进行第一次拦截,如果没有当前方法,应该如何处理:resolveInstanceMethod,让当前对象去补救,如果没有补救或者没有此方法,则返回NO,进入第二步。
PS,resolveInstanceMethod:的使用场景一般用来动态添加 setter 和 getter。

@implementation MethodObject
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    NSLog(@"为找到方法,进行第一次拦截%@",NSStringFromSelector(sel));
    if(sel == @selector(thisFirstMethd)){
        return class_addMethod(self, sel, (IMP)thisFirstMethd, "v@::");;
    }
    return [super resolveInstanceMethod:sel];
}
void thisFirstMethd(id self,SEL _cmd)
{
    NSLog(@"这是一个新增的方法,第一次拦截补救");
}
@end

2,第二步,将当前方法交给另外一个对象去补救,如果有补救,则返回补救的对象,如果没有则返回空。
PS,基于-forwardingTargetForSelector:,可以通过组合的方式,模拟出多继承的某些特性。
为协议遵循者提供默认实现,譬如某个协议定义了多个方法,有必要为这几个方法交给其他对象提供默认实现。

@implementation MethodObject
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(thisFirstMethd)){
        return  [[TransitTimer alloc]init];
    }
    return [super forwardingTargetForSelector:aSelector];
}
@end

//========
@implementation TransitTimer
- (void)thisFirstMethd
{
    NSLog(@"%@",@"这是第二次补救方法");
}
@end

3,- (NSMethodSignature *)methodSignatureForSelector,此方法会将当前类未实现的方法进行签名,添加到NSInvocation对象中,作为回调方法-forwardInvocation的回调参数,然后去处理当前类未实现的方法。

@implementation MethodObject
/* * 第三次拦截处理 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    /* 为指定的方法手动生成签名 */
    if(aSelector == @selector(thisFirstMethd)){
        //签名,将selector加入到NSMethodSignature生成的anInvocation对象中去
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    //获取当前的消息
    SEL sel = [anInvocation selector];
    //要处理selector的对象
    TransitTimer *tObject = [[TransitTimer alloc] init];
    /* 此注释为相同判断,当前类中是否有实现sel
    if([TransitTimer instancesRespondToSelector:sel]){
        //唤醒当前对象
        [anInvocation invokeWithTarget:tObject];
    }
     */
    if ([tObject respondsToSelector:sel]) {
        //唤醒当前对象
        [anInvocation invokeWithTarget:tObject];
    }else{
        [super forwardInvocation:anInvocation];
    }
    //当前类如果不执行任何操作,可以防止其找不到方法崩溃doesNotRecognizeSelector:异常
}
@end

这样,2、3也就完成了消息转发,1对消息进行了拦截解析,决策是否可以增加消息。

二十四,NSTimer为什么系统不会自动释放,为什么会造成循环引用,如何解决?

为什么不会被自动释放:
因为创建的NSTimer已经被加到了当前的Runloop里边,RunLoop持有当前Timer,RunLoop会对timer有强引用,所以要释放,必须得先从Runloop里边移除。
循环引用关系如下:
1,CurrentClass -> NSTimer
2,NSTimer -> target
3,NSTimer.target -> CurrentClass
造成了循环引用,所以不会去自动释放,只能手动调用Timer的invalidate方法释放,并且置空。

解决循环引用,参考连接:http://www.iashes.com/2020/11/20-576.html

二十五,对象和类的区别。

对象和类都是一个拥有指向objc_class类指针isa的结构体,类对象中结构体的isa指向元类,实例对象中的isa指向实例对象的类型。
类本身也是一个对象。
1,实例方法的resolveInstanceMethod 方法走的是类 — 父类 — 根类 — nil流程最终调用的是NSObject的+resolveInstanceMethod方法,类方法。
2,类方法的resolveClassMethod走的是元类 — 根元类 — 根类 — nil流程.最终也是调用NSObject的+resolveClassMethod方法;
那么就可以复写NSObject的+resolveInstanceMethod的方法实现崩溃拦截。

Leave a Reply

Required fields are marked *