面试亲历一些题 | 四

一,分类的加载顺序,取决于编译的顺序,编译的顺序取决于编译源(Compile Source)的排列顺序。

如果一个类中的方法,testMethod方法在类别中进行了重写,那么如果非要调用当前类中的方法而不去调用分类中重写的方法,需要进行二分法进行查找,根据当前类结构,获取类中的方法的IMP(方法指针),进行调用。

分类中+load方法的调用顺序:先调用其关联类父类的+load方法,然后调用其关联类的+load方法,最后调用分类中的+load方法。
子类也可以调用父类关联的类别中的方法。

+load方法在什么时候执行?

程序在启动时候,经历了三个阶段,编译、连接、运行。
连接阶段,就是将一个程序的所有目标程序和系统的库文件以及系统提供的其他信息连接起来,最终形成一个可执行的二进制文件。那么+load方法就是在连接时进行了调用执行。
当前执行时机在程序main函数之前进行执行的。
当动态连接器执行初始化方法的时候,会调用call_load_methods方法, 此方法中会先执行call_class_loads方法,然后执行call_category_loads方法,所以类内部的load方法执行先于分类中的load方法。

多个分类中重写一个方法,具体编译顺序是什么?

在Target – Build pahses – Complie Souces中进行查看编译顺序,也可以进行调换。

二,iOS锁类型。

1,互斥锁,防止两条线程同时对同一资源时导致数据或者资源的不完整;互斥锁分为递归锁和非递归锁。
互斥锁,如果当前资源已经被占用,资源申请者(当前第二个申请使用资源的线程)只能进入睡眠状态。
①,递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用;
②,非递归锁:不可重入,必须等待锁释放后才能再次获取锁。
互斥锁
2,自旋锁,同互斥锁一样,为了防止多个线程同时访问同一资源。不同的是,自旋锁不会导致第二个要访问资源的线程进入睡眠状态,它会一直循环等待第一个线程中的锁解开之后再去执行;
3,读写锁;

1,互斥锁:
①,pthread_mutex;
②,@synchronized互斥递归锁;
③,NSLock(对pthread_mutex的封装);
④,NSRecursiveLock;
⑤,NSCondition;
⑥,NSConditionLock;
⑦,os_unfair_lock。

2,自旋锁:
①,OSSpinLock;
②,atomic里边的自旋锁(spinlock_t)。

3,读写锁:pthread_rwlock_t。

信号量(semaphore)其实是一种特殊的互斥锁。
锁是服务于共享资源的,而semaphore是服务于多个线程间的执行的逻辑顺序的。
斥锁必须由单个线程获取和释放。
信号量是由单个线程释放,另一个线程获取,保证线程同步。

二,atomic和nonatomic区别。

同为属性修饰符。
atomic为原子性,也就是线程安全,在getter和setter方法内部为其成员变量进行了加锁,此锁为自旋锁(spinlock_t)。
nonatomic为非原子性,在getter方法里,直接返回其值,setter方法里新旧值进行了交换,并没有加锁。
注意:属性离开了setter,geter方法,它的原子性也不复存在,在外部需要开发者自行去处理。
在性能方法,nonatomic性能比atomic高,atomic会消耗很多资源,因为内部进行了一些锁处理,加了自旋锁,如果其他线程操作任务,那么此线程一直在循环等待。

三,为什么NSString要用copy去修饰?

@property (nonatomic,strong)NSString *strongStr;

首先,如果对NSString进行strong修复之后,在它被赋值之后,系统会自动将其变成一个NSMutableString类型的数据,怀疑是系统对其对象进行了mutableCopy处理,所以它是可以进行数据操作的:

    NSMutableString *mustring = [NSMutableString stringWithString:@"我的名字"];
    self.strongStr = mustring;
    NSLog(@"%@\n",self.strongStr);
    [mustring appendString:@"叫什么呢?"];
    [(NSMutableString *)self.strongStr appendString:@"en ne "];
    NSLog(@"%@\n%@",self.strongStr,mustring);
//输出
2020-11-28 17:43:07.19+0800 xxDemo[77179:4952430] 我的名字
2020-11-28 17:43:07.20+0800 xxDemo[77179:4952430] 我的名字叫什么呢?en ne 
我的名字叫什么呢?en ne

copy修饰不可变字符串,是为了防止将其对象赋值给其他变量时,其他变量的值进行改变,当前变量的值也会进行改变。
如果是进行copy,有两种场景:
1,首先对当前对象赋值给一个可变字符串:

@property (nonatomic,copy)NSString *strongStr;
//
NSMutableString *mustring = [NSMutableString stringWithString:@"我的名字"];
self.strongStr = mustring;

那么等同于self.strongStr = mustring.copy;这里的拷贝是深拷贝,因为它重新开辟了内存保存一份不可变类型对象的数据,然后让self.strongStr指向。

2,对当前对象赋值给一个不可变字符串:

@property (nonatomic,copy)NSString *strongStr;
//
NSString *mustring = @”我的名字”;
self.strongStr = mustring;

那么它也等同于self.strongStr = mustring.copy;但是是浅拷贝,只是让其指针指向了mustring对象的内存地址,但是没有开辟新的内存去保存一份值。

@property (nonatomic,copy)NSMutableString *muString;

copy修饰可变字符串的属性后,初始化后它会指向一个不可变的字符串NSString,因为遵循了其协议copying,实现copyWithZone方法的时候返回了NSMutableString的父类NSString。

四,assign是否能够修饰对象,如果不能,为什么?

assign可以修饰对象类型,但其作用一般用于修饰基本类型。
assign修饰对象,原则上是可以的,只是原则上,但是assign修饰对象在堆里,当对象被释放后,指针的地址还是存在的,也就是说指针并没有被置为nil,从而造成了野指针。如果后续的内存分配中,刚好分配到了这块内存,就会造成崩溃。所以用weak代替assign。
基本数据类型是分配在栈上的,由系统分配和释放,所以不会造成野指针。

五,weak修饰的属性,例如@property (nonatomic,weak)NSString *name;,在dealloc中进行了置空,会发生什么?

@property (nonatomic,weak)NSString *name;
//
self.name = @"my name is ...";
//
- (void)dealloc
{
    NSLog(@"this is - %@",self.name);
    self.name = nil;
    NSLog(@"this is - %@",self.name);
}

首先,当前weak指向的对象引用计数为0的时候,dealloc就会去执行,但是当对象的引用计数为0的时候,系统会将当前对象的weak指针在hash表中进行遍历然后通过setter方法进行置空(nil),这里再次置空也调用了其setter方法,在预编译的时候看了源码,((void *)0);, 当前weak指针会是是个野指针,因为其对象已经进行了释放,但是指针没有回收。

六,View的touch时间和tap事件哪个先执行?

Touch事件优先执行。
Touch Events是iOS中的触摸事件;
UIGestureRecognizer是iOS中的手势识别;
当UIGestureRecognizer事件传递和响应时,会通过方法:
– (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
– (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
进行传递和判断是否是当前View的事件,当识别出响应者之后,首相会调用响应者的响应事件,也就是Touch事件,
首先调用touchesBegan事件,然后调用收拾识别,等待离开手势识别结束后,会调用touchesCancelled事件。

事件传递,可通过下方法进行判断传递给不同的View。
– (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
– (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

子视图超出父视图范围解决:

重写父View的- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event方法,判断触摸点,然后交给其View执行。

七,websocket丢包怎么解决?

首先,websocket的协议是在TCP/IP协议簇的应用层,和HTTP在同一层,是基于TCP/IP层的,没有基于UDP的。

首先,在每次连接socket之前,用Http去请求因为离线儿积累的消息数据;
然后连接socket,连接成功之后,和后台规定好心跳包的消息数据,按时去发送。
其次当客户端发送消息给服务器的时候,消息里边增加一个是否传输成功的标识,发送给服务器,服务器返回当前接收的消息是否接受成功;
服务器发送消息的时候,也带一个传输标识,客户端接收成功之后,告诉服务器当前消息传输成功了。
然后断开连接的时候,发送一个约定好的数据,先告诉服务器我要断开了,因为如果客户端断开的时候,可能会有延迟,服务器还没有收到断开的消息,还会发送数据给客户端,所以断开连接之前告诉服务器我要断开了,让服务器做好准备,然后断开。

八,A类是B类的父类,运行时,B类的方法和A类的方法进行调换,会不会对A类造成影响,如果会,则会有什么影响?(防止数组越界)

@interface MethodObject_1 : NSObject
- (void)methodOp;
@end
//
@implementation MethodObject_1
- (void)methodOp
{
    NSLog(@"MethodObject_1");
}
@end
------------------------
@interface MethodObject_2 : MethodObject_1
- (void)methodOp;
@end
//
@implementation MethodObject_2
- (void)methodOp
{
    [super methodOp];
    NSLog(@"MethodObject_2");
}
@end
//实现
    MethodObject_1 *obj_1 = [[MethodObject_1 alloc]init];
    MethodObject_2 *obj_2 = [[MethodObject_2 alloc]init];
    [obj_1 methodOp];
    [obj_2 methodOp];
    Method oriMethod1 = class_getInstanceMethod(obj_1.class, @selector(methodOp));
    Method swiMethod1 = class_getInstanceMethod(obj_2.class, @selector(methodOp));
    method_exchangeImplementations(oriMethod1, swiMethod1);
    [obj_1 methodOp];
    [obj_2 methodOp];

父类和子类都有相同的方法,相互调换没什么问题,只是如果在子类中要实现父类的方法,会造成死循环。

数组越界解决办法:

#import "NSArray+Index.h"
#import <objc/runtime.h>

@implementation NSArray (Index)
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = objc_getClass("__NSArrayI");
        SEL originalSelector = @selector(objectAtIndex:);
        SEL swizzledSelector = @selector(AS_objectAtIndex:);
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        BOOL didAddMethod = class_addMethod(class,originalSelector,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {
            class_replaceMethod(class,swizzledSelector,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));
        }else{
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        //
        
        SEL originalSelector_1 = @selector(objectAtIndexedSubscript:);
        SEL swizzledSelector_1 = @selector(AS_objectAtIndexedSubscript:);
        Method originalMethod_1 = class_getInstanceMethod(class, originalSelector_1);
        Method swizzledMethod_1 = class_getInstanceMethod(class, swizzledSelector_1);
        BOOL didAddMethod_1 = class_addMethod(class,originalSelector_1,method_getImplementation(swizzledMethod_1),method_getTypeEncoding(swizzledMethod_1));
        if (didAddMethod_1) {
            class_replaceMethod(class,swizzledSelector_1,method_getImplementation(originalMethod_1),method_getTypeEncoding(originalMethod_1));
        }else{
            method_exchangeImplementations(originalMethod_1, swizzledMethod_1);
        }
    });
}

- (id)AS_objectAtIndex:(NSInteger)index
{
    if(index > self.count - 1){
        NSLog(@"%@",[NSString stringWithFormat:@"%s,数组越界了",__func__]);
        return nil;
    }
    //As_objectAtIndex跟objectAtIndex进行了交换,所以会调用objectAtIndex方法返回对象
    return [self AS_objectAtIndex:index];
}
- (id)AS_objectAtIndexedSubscript:(NSInteger)index
{
    if(index > self.count - 1){
        NSLog(@"%@",[NSString stringWithFormat:@"%s,数组越界了",__func__]);
        return nil;
    }
    //As_objectAtIndex跟objectAtIndex进行了交换,所以会调用objectAtIndex方法返回对象
    return [self AS_objectAtIndexedSubscript:index];
}

@end

九,__weak和__unsafe_unretained的区别。

__weak:当对象被引用时对象的引用计数不会加1,当对象销毁时,weak指针被置空nil;
__unsafe_unretained:当对象被引用时对象的引用计数不会加1,当对象销毁时,当前指针没有被置空,指向原来对象的地址,出现野指针。
__strong,strong修饰的成员变量对其新值引用计数加一,然后如果重新复制,当前strong对象的指针指向新值,对其旧值进行释放。

__weak修饰的对象,首先通过objc_initWeak进行初始化,将获取当前被修饰对象的指针,然后内部通过调用objc_storeWeak方法,生成一个全局弱引用表,它是一个hash表,然后将当前对象的指针作为key保存起来,然后将weak指向对象的弱引用指针保存在一个数组中(这里数组保存的元素是结构体类型),作为hash表的value,因为可能对象有多个weak指针。
当weak引用指向的对象引用计数为0时,会调用dealloc被释放,调用dealloc时会调用clearDeallocating方法:
根据对象地址获取所有weak指针地址的数组元素,然后遍历将weak指针置为空nil,然后再删除当前hash表中保存的对象指针,也就是key。
weak释放过程:
1、从weak表中获取废弃对象的地址为键值的记录;
2、将包含在记录中的所有附有 weak修饰符变量的地址赋值为nil;
3、将weak表中该记录删除;
4、从引用计数表中删除废弃对象的地址为键值的记录。

延伸:SideTable结构体中,有两张表,一张引用计数表,一张weak全局表,引用计数表为:RefcountMap refcnts,也是一张哈希表,哈希中的key为当前弱引用对象的地址,value为引用计数,所以refcnts表中,引用计数value中标识当前对象为weak引用的对象,从而得知当前对象是否是弱引用对象。

如下会造成什么情况:

@property (nonatomic, strong)NSString *string;
//
dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 100 ; i++) {
        dispatch_async(queue, ^{
            self.string = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
        });
    }

会造成崩溃,并发执行,多个线程进行操作当前对象,导致对象指向的内存地址同时释放了多次,当前指针会变成野指针。

十,NSNotificationCenter通知的几个注意点。

1,注册观察者必须要在发送通知之前进行注册;
2,发送通知在哪个线程,则回调的@selector就在哪个线程;
3,发送通知时传递的object,注册时如果不传递object,那么回调的时候也能得到此数据。

[center postNotificationName:@"TEST" object:@"123"];
//
[center addObserver:self selector:@selector(doAction:) name:@"TEST" object:nil];
//
- (void)doAction:(NSNotification *)noti
{
    NSLog(@"%@",noti.object);
    NSLog(@"%s - %@ - %@",__FUNCTION__,noti.object,[NSThread currentThread].description);
}
//输出123

4,如果发送通知时没有传递object,注册时有传递object,那么当前通知的回调方法会找不到,所以不会去执行通知的回调@selector;
5,在注册通知时,如果不去写name,那么会接受当前所有已经发出的通知。

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(doAction:) name:nil object:nil];
- (void)doAction:(NSNotification *)noti
{
    NSLog(@"%@",noti.name);
    NSLog(@"%s - %@ - %@",__FUNCTION__,noti.object,[NSThread currentThread].description);
}
//输出
2020-12-01 xxxxDemo[88518:6133224] UINavigationControllerWillShowViewControllerNotification
2020-12-01 xxxxDemo[88518:6133224] -[ThirdViewController doAction:] - <UINavigationController: 0x7ff582820000> - <NSThread: 0x60000103cb40>{number = 1, name = main}
2020-12-01 xxxxDemo[88518:6133224] _UIApplicationDidBeginIgnoringInteractionEventsNotification
2020-12-01 xxxxDemo[88518:6133224] -[ThirdViewController doAction:] - <UIApplication: 0x7ff581c04de0> - <NSThread: 0x60000103cb40>{number = 1, name = main}
2020-12-01 xxxxDemo[88518:6133224] _UIApplicationDidEndIgnoringInteractionEventsNotification
2020-12-01 xxxxDemo[88518:6133224] -[ThirdViewController doAction:] - <UIApplication: 0x7ff581c04de0> - <NSThread: 0x60000103cb40>{number = 1, name = main}
2020-12-01 xxxxDemo[88518:6133224] UINavigationControllerDidShowViewControllerNotification
2020-12-01 xxxxDemo[88518:6133224] -[ThirdViewController doAction:] - <UINavigationController: 0x7ff582820000> - <NSThread: 0x60000103cb40>{number = 1, name = main}
2020-12-01 xxxxDemo[88518:6133224] UITextInputSourceDidChangeNotification
2020-12-01 xxxxDemo[88518:6133224] -[ThirdViewController doAction:] - (null) - <NSThread: 0x60000103cb40>{number = 1, name = main}
2020-12-01 xxxxDemo[88518:6133299] NSThreadWillExitNotification
2020-12-01 xxxxDemo[88518:6133299] -[ThirdViewController doAction:] - <NSThread: 0x600001078640>{number = 5, name = (null)} - <NSThread: 0x600001078640>{number = 5, name = (null)}
2020-12-01 xxxxDemo[88518:6133303] NSThreadWillExitNotification
2020-12-01 xxxxDemo[88518:6133303] -[ThirdViewController doAction:] - <NSThread: 0x600001016880>{number = 4, name = (null)} - <NSThread: 0x600001016880>{number = 4, name = (null)}
2020-12-01 19:28:14.577921+0800 TimerMemoryDemo[88518:6133304] NSThreadWillExitNotification
2020-12-01 xxxxDemo[88518:6133304] -[ThirdViewController doAction:] - <NSThread: 0x600001017300>{number = 3, name = (null)} - <NSThread: 0x600001017300>{number = 3, name = (null)}

十一,系统瘦身。

1,删除无用的资源文件,可用LSUnusedResources;此工具会通过正则去查询当前文件是否被引用;
2,文件去重,fdupes工具;
3,大文件进行压缩,在不影响用户体验的情况下。主要是图片文件,用ImageOptim工具进行无损压缩。当然如果是MP3或者其他本地视频文件,可用其他应用进行无损压缩。
Xcode在TARGETS-Build Settings中提供了PNG图片压缩选项,可在打包app的时候,自动进行压缩,
Remove Text Medadata From PNG Files 选项是对图片进行的一些瘦身,可在打包的时候自动删除图像名称、作者、版权、创作时间、注释等信息;
4,图片管理方式规范。工程中的.xcassets下图片归Assets.car,不在 Assets.car 内的贵Bundle进行管理,.xcassets下的图片之只能通过imageNamed: 进行加载,Bundle中的还可以用imageWithContentsOfFile:进行加载。所以,.xcassets加载后的图片会将位图缓存在内存中。所以:
①,.xcassets中避免使用.jpg的图片,因为.xcassets中的.jpg图片打包后,系统会自动转换成.png,并且内存比之前.jpg的时候大。
②,.xcassets尽量存放小图,因为会生成内存缓存,占用内存;尽量不要放.jpg的图片。
③,Bundle中可以放大图,可以使用.jpg的图。
5,编译优化;
6,文件优化,删除无用类、无用方法、重复方法等,来达到可执行文件大小的减小,fui;
文件,方法,参数,变量的优化,可以将其命名长度不要太长,编译的时候也占用很多内存资源。
7,App Extension对扩展进行优化,扩展的描述参考:
https://www.cnblogs.com/junhuawang/p/8178276.html

十二,野指针。

指针其所指向的对象被释放或者收回,但是指针没有被收回,或者没有被置空nil,此指针被称为野指针。
例如__unsafe_unretained修饰的变量指针。
野指针定位:暂无总结。

Leave a Reply

Required fields are marked *