面试亲历一些题 | 二

一,KVO原理,如何实现?

所谓KVO,也就是键值监听。
首先,KVC也能触发KVO,因为KVC内部也是通过消息转发机制调用其对象的get,set方法。
原理:
1,当一个对象使用了KVO监听,系统会通过Runtime动态创建一个当前对象的子类对象NSKVONotifyin_Person(例如被监听类为Person),当然创建了新的子类也会有对应的结构体。然后修改这个当前被监听对象的isa指针,使其指向一个全新的子类地址;
NSKVONotifyin_Person是继承于被监听类Person的。
也就是当前类的指针,指向了系统自动创建的一个被监听类的子类,子类Class结构中包含:
isa //指向类对象的地址
superclass //父类
setAge: //成员变量的set方法
class //class方法
dealloc //销毁方法
_isKVOA //KVO标识
……

    MethodObject *mObj = [[MethodObject alloc]init];
    NSLog(@"%@",object_getClass(mObj));
    [mObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:nil];
    mObj.name = @"this name value";
    NSLog(@"%@",object_getClass(mObj));
//输出NSKVONotifying_MethodObject,是系统自动创建的子类对象
2020-11-18 21:46:42.486295+0800 xxxxxDemo[30124:1670864] MethodObject
2020-11-18 21:46:42.487084+0800 xxxxxDemo[30124:1670864] NSKVONotifying_MethodObject

所以,当对象被监听后,被监听对象的地址改变了,那么它所拥有的成员变量的set方法也不是之前对象的set方法,系统内部子类重写了当前set方法,并且进行了重构和特殊处理。

2,子类拥有自己的set方法实现,set方法实现内部会顺序调用:
①,willChangeValueForKey方法;
②,对象成员变量的赋值方法;
③,didChangeValueForKey方法;
而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法,来监听当前被监听的对象属性。
从上而知,那么就可以手动去触发KVO,例如:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    NSLog(@"%@",change);
}
{
    MethodObject *mObj = [[MethodObject alloc]init];
    [mObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionOld |NSKeyValueObservingOptionNew context:nil];
    [mObj willChangeValueForKey:@"name"];
    [mObj didChangeValueForKey:@"name"];
}
//输出:
2020-11-18 21:37:10.814323+0800 xxxxxDemo[30031:1663741] {
    kind = 1;
    new = "<null>";
    old = "<null>";
}

二,二分法,二分查找法。

首先是一个有序数组,如果不是先排序。然后定义两个指针,*a指向头角标,*b指向尾角标,通过循环,循环的结束条件为,头指针大于或者等于尾指针的时候,循环结束;
循环时,先取下标的中间值,中间值为(a+b)/2,然后将判断,如果当前中间值所在的下标值等于要查询的值,直接返回;
如果小于当前要查询的值,那么头角标向后移动一位;
如果大于当前要查询的值,那么尾角标向前移动一位;
直到所查询的值相同,后返回。

public static int Method(int[] nums, int low, int high, int target)
        {
            while (low <= high){
                int middle = (low + high) / 2;
                if (target == nums[middle]){
                    return middle;
                }else if (target > nums[middle]){
                    low = middle + 1;
                }else if (target < nums[middle]){
                    high = middle - 1;
                }
            }
            return -1;
        }

三,类别是如何实现的?

首先,分类的结构体指针中,没有属性(成员变量)列表,只有方法列表。所以只能添加方法,不能添加属性(成员变量),不过也可通过其它方式添加属性。

//分类的结构体
struct _category_t {
    const char *name;//类名
    struct _class_t *cls;//类
    const struct _method_list_t *instance_methods;//category中所有给类添加的实例方法的列表(instanceMethods)
    const struct _method_list_t *class_methods;//category中所有添加的类方法的列表(classMethods)
    const struct _protocol_list_t *protocols;//category实现的所有协议的列表(protocols)
    const struct _prop_list_t *properties;//category中添加的所有属性(instanceProperties)
};

分类中的可以写@property属性,因为它结构体里边有属性列表, 但不会生成setter/getter方法,也不会生成成员变量。
系统会将分类编译时的Mach-O在加载到内存的时候,将分类和对应的类进行绑定。

作用:
1,扩展已有的类;
2,引用父类未公开方法;

分类是运行时关联分类和类中方法或者其属性,编译时只会将它变成可执行文件。
编译 -> 链接 -> 运行。
那么在编译后的链接过程中,系统将category中的方法,属性,协议都绑定在了元类上,并且会将方法,属性,协议去进行合并。

分类中方法调用的优先级:分类方法列表->原来的类方法列表->父类方法列表
所以,如果分类中重写了本类的方法,会去执行分类里边的方法,而不去执行本类的当前方法。
如果同时给Person类创建了两个类别,两个类别都有student方法,那么会根据两个分类的编译顺序,哪个方法最后编译,就去执行那个student方法。

原理:创建一个分类,系统会在编译的时候自动生成此分类的结构体category_t,将分类的方法列表等信息存入这个结构体。在编译阶段分类的相关信息和本类的相关信息是分开的。
等到运行阶段,会通过runtime加载某个类的所有Category数据,把所有Category的方法、属性、协议数据分别合并到一个数组中,然后再将分类合并后的数据插入到本类的数据的前面。这也就是为什么去执行分类中的方法而不去执行类里边的方法的缘故。
在编译阶段,结构体中的类别的方法列表_method_list_t和类的方法列表objc_method_list是分开存储的。
在运行阶段,系统会将分类的方法列表存储在一个数组里边,如有两个分类a,b,如a分类先编译,然后编译b分类,那么这个组合的数组里边,b的方法列表在前,例如:
Category_Methods = @[b_methods,a_methods];这也就是说明了,如果有两个分类,同时有一个student方法,会执行后边编译的分类方法,而不是执行先编译的分类方法。
最后,运行时会将分类和类进行挂链,然后将所有分类的方法、类的方法放在一个数组中,类的方法在数组的末端,再根据顺序判断执行哪个方法。如:
objc_method_list = @[ b_methods,a_methods,objc_method_list(类本身之前的方法列表)];
最后会执行b_methods里边的方法。
所以类别增加的方法,并没有覆盖类本身的方法,只不过分类方法数据在类方法之前,所以进行了调用。
重点:类别编译的时候,将确定的方法列表保存在类别结构体中的_method_list_t(方法列表中),运行时,会合并到关联类的方法列表objc_method_list中,并且放在当前类中的方法前边,所以会先去调用分类里边的方法。
属性,协议同理。

category中如果重写了+Load方法,在程序启动的时候,首先会调用类里的Load方法,然后再调用类别里边的Load方法。
category中如果重写了+initialize方法,在第一次使用当前类的时候,先调用分类中的initialize方法,其并不会覆盖类中的initialize方法,只是不会调用类中的initialize方法。
总结:
+Load是应用加载类、分类的时候(程序启动的时候)调用,且每个类只会调用1次;
initialize是类第一次接收到消息的时候(第一次使用当前类的时候)调用,每一个类只会initialize一次,如果子类实现了父类的initialize方法,那么父类的initialize可能会调用多次;
Load是根据函数地址直接调用,initialize是通过objc_msgSend调用。

扩展:本类中的+Load方法中能否调用分类中的方法?
是可以调用的,因为本类在运行+Load之前就已经将分类中的方法,协议,属性等绑定在了此类的元类中。

为什么类别中不能够添加属性?
其实是可以添加属性的,但是系统没有生成属性的get和set方法,因为类别结构体category_t中并不存在成员变量列表(属性列表)。

分类中将成员列表和对象进行关联:
objc_setAssociatedObject(self, @”name”, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_getAssociatedObject(self, @”name”);

四,远程推送的原理。

1,Apple设备获取devicetoken,传递给后台,然后后台传递给ANPS服务器。APNS 服务器会验证devicetoken,连接成功后会与设备创建一个基于TCP 的长连接;
2,触发推送,后台会将我们的推送消息发送给APNS服务器;
3,APNS服务器将推送信息推送给指定devicetoken的iOS设备;
4,设备收到推送。

五,图片如何加水印?

@implementation UIImage (Circle)
// 1,给图片添加文字水印:
@implementation UIImage (Circle)
- (instancetype)photoWatermarkByText:(NSString *)text atPoint:(CGPoint)point attributedString:(NSDictionary * )attributed
{
    @autoreleasepool {
        //1.开启上下文
        UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
        //2.绘制图片
        [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
        //添加水印文字
        [text drawAtPoint:point withAttributes:attributed];
        //3.从上下文中获取新图片
        UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
        //4.关闭图形上下文
        UIGraphicsEndImageContext();
        //返回图片
        return newImage;
    }
}
@end
@implementation UIImage (Circle)
//添加图片水印
- (instancetype)photoWatermarkByAddMsakImage:(UIImage *)maskImage
{
    UIGraphicsBeginImageContextWithOptions(self.size ,NO, 0.0);
    [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
    //四个参数为水印图片的位置
    [maskImage drawInRect:CGRectMake(0, 0, self.size.width, self.size.height/2)];
    //如果要多个位置显示,继续drawInRect就行
//    [maskImage drawInRect:CGRectMake(0, self.size.height/2, self.size.width, self.size.height/2)];
    UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return resultingImage;
}
@end

六,websocket的原理,以及发送心跳。

websocket,是在Html协议上层封装的一层协议,是html5出的一个持久化连接协议。可以理解为,WebSocket 是 HTTP 协议为了支持长连接的一个升级版本。
Html于websocket的不同:html不能主动联系客户端,只能有客户端发起请求,服务器端才能进行相应。
Html需要不断地建立HTTP连接。
——————————————–
websocket连接,服务端就可以主动推送信息给客户端。
只需要建立一次HTTP请求。

心跳:心跳机制是每隔一段时间会客户端向服务器发送一个数据包,告诉服务器当前设备没有断开连接,同时客户端会确认服务器端是否正常服务。
心跳规则:在规定的时间内,向服务器发送规定的数据,让服务器知道当前数据是登陆状态的数据,这个得跟后台开发人员商议好数据规则。
例如:App端每隔1分钟向服务器发送当前token+time数据,然后服务器看见这个数据后,知道当前用户(设备)还在连接,返回一些规定的数据,例如time+当前连接的标示,告诉客户端,我知道你没有断开,并且我服务器端也可正常使用。
当然,在发送成功或者接受成功的时候,也就不需要进行心跳检测了,需要重置当前的心跳检测机制,避免重复发送数据浪费资源。

七,数组越界怎么拦截让其不崩溃。

写一个NSArray或者NSMutableArray的类别,让其重新去做添加元素、获取元素,删除元素的方法,然后在方法里做一些判断,实际用的时候调用此方法即可。例如:

https://www.jianshu.com/p/b0d3a64e76a2

八,单向链表怎么输出倒数第二个值。

单向链表只能单向读取。
首先定义两个指针 *a,*b,然后让b指向下两个节点,b->next; b->next,然后创建一个循环,遍历当前链表,循环的结束条件为b != NULL,因为尾节点的指针为空。在循环里边将头节点的指针每循环一次都指向下一个a->next,直到b ==NULL的时候,那么a就是指向倒数第二个值的指针。

    //使用两个指针,通过移动指针,遍历一次链表,p指针首先移动n-1步,然后p和q同时移动,知道p.next == null,此时p所指向的节点就是所求
    Node p = head;
    Node q = head;
    while(--n != 0 && q != null) {
        q = q.next;
    }        
    if (q == null)
        return null; // n大于链表长度
    while(q.next != null) {
        q = q.next;
        p = p.next;
    }
    return p;

九,视频推流的过程是什么?

1,采集数据,也就是音视频,然后处理,美颜或者特效等;
2,把音视频数据使用传输协议进行封装,变成流数据;
3,然后通过一定的Qos算法将音视频流数据推送到网络端,后台进行分发进行分发。

十,这样会不会触发离屏渲染?

首先离屏渲染为什么消耗GPU资源,导致卡顿?
离屏渲染是GPU在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作。操作完之后将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。这两步是特别消耗GPU资源的,导致屏幕刷新率跟不上,所以卡顿。

imageView.layer.cornerRadius = 5;
imageView.layer.masksToBounds = YES;
iOS9.0之后对UIImageView的圆角设置做了优化,UIImageView这样设置圆角,不会触发离屏渲染,iOS9.0之前还是会触发离屏渲染。而UIButton还是会触发离屏渲染。

解决方案:

1,如果设置阴影,再去设置Layer的shadowpath,则不会造成离屏渲染:
view.layer.shadowPath = [UIBezierPath pathWithCGRect:view.bounds].CGPath;
2,设置masksToBounds = true;cornerRadius > 0时,会产生离屏渲染,如果是UIView,直接绘制圆角图形:
- (void)setRadius:(CGFloat)radius
{
    if(radius <= 0) return;
    _radius = radius;
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(self.radius, self.radius)];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    [self.layer setMask: maskLayer];
}
3,可以直接绘制圆形;
- (void)drawCircle
{
    if(CGRectEqualToRect(self.bounds, CGRectZero)){
        [self.superview layoutIfNeeded];
    }
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:self.bounds.size];
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
    maskLayer.frame = self.bounds;
    maskLayer.path = maskPath.CGPath;
    self.layer.mask = maskLayer;
}
4,对image自身进行圆角绘制
- (void)imageByCircleWithFinally:(void(^)(UIImage *fImage))finally
{
    @autoreleasepool {
        dispatch_queue_t userQueue = dispatch_queue_create("com.ashes.imageQueue", NULL);
            dispatch_async(userQueue, ^{
                NSLog(@"%@",[NSThread currentThread].description);
                UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
                UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
                [path addClip];
                [self drawAtPoint:CGPointZero];
                UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();
                dispatch_async(dispatch_get_main_queue(), ^{
                    if(finally) finally(image);
                });
            });
    }
}

十一,OC的动态性主要体现在哪里?

1,动态类型,id类型的对象,就是动态性的体现,在程序运行时才会识别它的类型;
2,动态加载,程序中的一些资源文件,例如图片,Html,不会进行预编译,只是会在用到的时候去加载;
3,动态绑定,程序中的函数或者方法,运行的时候动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去,不会提前进行编译。

十二,图文混排的实现。

1,NSMutableAttributedString;
2,CoreText.Framework;
绘画流程:
确定当前string或attributedString,然后生成 CTFramesetter -> 得到CTFrame -> 绘制。
3,三方库,DTCoreText;
https://github.com/Cocoanetics/DTCoreText
CoreCoreText实现示例如下:

- (void)drawRect:(CGRect)rect {
    // Drawing code
    NSString *str = @"This is the CoreText.Framework by Text. 我想先测测";
    NSMutableAttributedString *mabstring = [[NSMutableAttributedString alloc]initWithString:str];
    [mabstring beginEditing];
    NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithObject:(id)[UIColor redColor].CGColor forKey:(id)kCTForegroundColorAttributeName];
    //斜体
    CTFontRef font = CTFontCreateWithName((CFStringRef)[UIFont italicSystemFontOfSize:20].fontName, 40, NULL);
    [attributes setObject:(__bridge id)font forKey:(id)kCTFontAttributeName];
     //下划线
    [attributes setObject:(id)[NSNumber numberWithInt:kCTUnderlineStyleDouble] forKey:(id)kCTUnderlineStyleAttributeName];
    
    //空心字
    long number = 2;
    CFNumberRef num = CFNumberCreate(kCFAllocatorDefault,kCFNumberSInt8Type,&number);
    [attributes setObject:(__bridge id)num forKey:(id)kCTStrokeWidthAttributeName];
    //添加string到绘画属性
    [mabstring addAttributes:attributes range:NSMakeRange(0, mabstring.length)];
    NSRange kk = NSMakeRange(0, mabstring.length);
    NSDictionary * dc = [mabstring attributesAtIndex:0 effectiveRange:&kk];
    [mabstring endEditing];
          
    NSLog(@"value = %@",dc);
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)mabstring);
          
    CGMutablePathRef Path = CGPathCreateMutable();
    CGPathAddRect(Path, NULL ,CGRectMake(10 , 0 ,self.bounds.size.width-10 , self.bounds.size.height-10));
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), Path, NULL);
          
    //获取当前(View)上下文以便于之后的绘画,这个是一个离屏。
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetTextMatrix(context , CGAffineTransformIdentity);
          
    //压栈,压入图形状态栈中.每个图形上下文维护一个图形状态栈,并不是所有的当前绘画环境的图形状态的元素都被保存。图形状态中不考虑当前路径,所以不保存
        //保存现在得上下文图形状态。不管后续对context上绘制什么都不会影响真正得屏幕。
    CGContextSaveGState(context);
          
    //x,y轴方向移动
    CGContextTranslateCTM(context , 0 ,self.bounds.size.height);
          
    //缩放x,y轴方向缩放,-1.0为反向1.0倍,坐标系转换,沿x轴翻转180度
    CGContextScaleCTM(context, 1.0 ,-1.0);
    //开始画
    CTFrameDraw(frame,context);
    //释放
    CGPathRelease(Path);
    CFRelease(framesetter);
}

十三,MVC,MVVM。

MVC,MVVM都是一种设计思想。
MVC是通过V层调度和连接V和M,Model主要负责存储数据和业务逻辑处理,View负责显示数据与用户进行交互,Controller作为中间者,协调Model和View相互协作。
1,C层能够访问Model和View层,但是M和V不能相互访问;
2,当View于用户进行交互时,使用target-action的方式;
3,View处理一些特殊的逻辑或者或者数据源时,需要通过代理的方式交给C层去处理,例如delegate,datasource;
4,当Model有数据更新时,通过Notification或者KVO来告知C层去处理V层视图。
MVVM是通过中间层ViewModel去处理业务逻辑。其主要是为了:分离视图和模型,从而达到低耦合度。
1,MVVM将业务处理逻辑交给了ViewModel层;
2,Model层保存原始的请求数据,数据持久化;
3,View层,视图展示,由ViewController层控制;
4,ViewModel层负责网络请求,业务处理,数据转换等一些逻辑业务。

MVVM的具体分工:

1,Model 层,数据层,做一些对基础实体的定义,数据的远程获取,对本地数据的操作(缓存)等。
2,View 层,,视图展示层。视图展示层,将视图从Controller抽离之后, 在 Controller做的事情(业务逻辑)都转移到了 ViewModel 中。
3,ViewModel 层,业务逻辑层,,处理一切与 UI 状态处理无关的业务处理。Model中获取的实体数据,ViewModel 层直接执行对应的方法让 View 重新绘制视图。
ViewModel将block作为viewmodel封装的方法的形参,将处理后的结果进行返回给Controller,也是对Controller的瘦身,通过Controller通知更新视图。
4,Controller层,专门负责管理视图。

十四,iOS中手势冲突了,怎么去解决?

1,重写手势代理
//重写UIGestureRecognizerDelegate,截取当前事件view,然后进行处理。
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch  
{  
    // 输出点击的view的类名  
    NSLog(@"%@", NSStringFromClass([touch.view class]));  
    // 若为UITableViewCellContentView(即点击了tableViewCell),则不截获Touch事件  
    if ([NSStringFromClass([touch.view class]) isEqualToString:@"UITableViewCellContentView"]) {  
        return NO;  
    }  
    return  YES;  
} 
2,设置手势优先级
//设置手势优先级,避免手势冲突
UIPanGestureRecognizer *panGes = [tableView.superview.gestureRecognizers objectAtIndex:0];
[panGes requireGestureRecognizerToFail:cell.leftSwipe];
[panGes requireGestureRecognizerToFail:cell.rightSwipe];
[A requireGestureRecognizerToFail:B]函数,它可以指定当A手势发生时,即便A已经滿足条件了,也不会立刻触发,会等到指定的手势B确定失败之后才触发。

十五,iOS事件响应流程。

1,程序发生触摸事件后,系统利用Runloop将事件加入到UIApplication的任务队列中。
2,UIApplication分发触摸事件到UIWindow,然后UIWindow依次向下分发给UIView。
3,View通过调用hitTest:withEvent:判断自己是是否能处理当前事件,在这之前,View会调用pointInside:curP withEvent:event方法判断当前触摸点是不是在自身。
4,如果当前View的条件满足,则遍历View上边的子控件,重复上边的操作,直到找到最终的子控件。
5,那么当前控件就是第一响应者,然后去处理响应事件。

十六,将一个可变数组进行copy操作后,会如何?

1,属性copy去修饰的可变数组;

@property (nonatomic,copy)NSMutableArray *mArr;
//初始化
self.mArr = [NSMutableArray array];

初始化之后,用copy修饰的可变数组变成了一个不可变的数组,如图:


当使用copy修饰属性时,会调用其类的-copyWithZone:方法,它会在内部将当前对象转换为不可变的对象。目测里边的实现方式为:

- (instancetype)copyWithZone:(NSZone *)zone
{
    return [[[self class] alloc]init];
}

OC具有多态性,self.class最终指向了父类,所以在运行时阶段其isa指向的是[NSArray Class]对象,也就是其父类NSArray,不可变数组。
那么对这个不可变数组进行元素的增删改,会造成奔溃,
*** Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[__NSArray0 addObject:]: unrecognized selector sent to instance 0x7fff8002e300’
提示当前不可变数组没有addObject方法,所以崩溃。
当前代码类似于:

@property (nonatomic,copy)NSMutableArray *mArr;
//初始化
NSMutableArray *tmp_arr = [NSMutableArray array];
//copy之后,重新生成一个地址,保存不可变的对象,交给了self.mArr。
self.mArr = tmp_arr.copy;

那么给一个可变数组类型的局部变量进行copy,会如何?

NSMutableArray *mmArr = [NSMutableArray array].copy;
查看后,还是会变成(生成)一个不可变的数组,那么当然给它进行数据元素的增删改也会崩溃了,这样其实是重新生成了一份内存,保存新拷贝的mmArr变量,如图:

解析:给一个可变数组变量进行copy之后,会重新分配内存给一个不可变的数组,并且当前变量指向不可变数组的内存地址,所以copy之后的赋值的变量为不可变数组,不能够进行增删改,但是可变数组还可以进行增删改,例如:

    NSMutableArray *sArray = [NSMutableArray array];
    id ay = sArray.copy;
    NSLog(@"%p - %p",&sArray,&ay);
    //可变数组仍能操作
    [sArray addObject:@"123"];
    //打印它们两个的内存地址输出
(lldb) p sArray
(__NSArrayM *) $0 = 0x0000600001de3750 @"1 element"
(lldb) p ay
(__NSArray0 *) $1 = 0x00007fff8002e300 @"0 elements"
//例子:
//定义一个不可变数组
NSArray *array = [NSArray arrayWithObjects:@"1",@"2", nil];
//对其进行copy,那么arr_cp变量的指针指向和array指针指向是同一个地方,也就是说指针指向的内存地址是相同的,只是复制了一份内存指向,而没有开辟内存
NSArray *arr_cp = array.copy;
//如果对array进行深拷贝,那么它开辟了一段新的内存保存当前arr_mcp的实体数据,从而根据多态性,arr_mcp会成为一个可变数组。
NSArray *arr_mcp = array.mutableCopy;
//打印输出
NSLog(@"%p\n%p\n%p\n",&array,&arr_cp,&arr_mcp);
//当前为浅拷贝,等同于arr_cp = array.copy
NSMutableArray *m_arr_cp = array.copy;
//当前为深拷贝,等同于arr_mcp = array.mutableCopy
NSMutableArray *m_arr_mcp = array.mutableCopy;
NSLog(@"%p\n%p\n%p\n",&array,&m_arr_cp,&m_arr_mcp);
//----------输出的地址分别为-----------
(lldb) print array
(__NSArrayI *) $0 = 0x0000600000149980 @"2 elements"
(lldb) print arr_cp
(__NSArrayI *) $1 = 0x0000600000149980 @"2 elements"
(lldb) print arr_mcp
(__NSArrayM *) $2 = 0x0000600000f3cd20 @"2 elements"
(lldb) print m_arr_cp
(__NSArrayI *) $3 = 0x0000600000149980 @"2 elements"
(lldb) print m_arr_mcp
(__NSArrayM *) $4 = 0x0000600000f2f4b0 @"2 elements"

那么,如果将一个可变数组赋值给一个不可变数组,会发生什么?

NSMutableArray *mutabArr = [NSMutableArray arrayWithObjects:@"m_1",@"m_2", nil];
NSArray *nArray = mutabArr;
NSLog(@"%@",mutabArr,nArray);
//
(lldb) print mutabArr
(__NSArrayM *) $0 = 0x000060000323eee0 @"2 elements"
(lldb) print nArray
(__NSArrayM *) $1 = 0x000060000323eee0 @"2 elements"

重点:当将一个可变数组赋值给不可变数组时,因为直接赋值属于浅拷贝,那么当前不可变数组指向的内存地址也是赋值的可变数组的内存地址,只是复制了一份指针。那么当前不可变数组根据多态性,NSArray对象指向了NSMutableArray的对象地址,所以变成了可变数组。从而得知,如果mutabArr数组元素数据变化,nArray数据也会跟着变化,因为它们公用同一份数据。

如果将不可变数组,通过下列方式赋值给可变数组:

NSArray *s_n_array = [NSArray arrayWithObjects:@"n_1",@"n_2", nil];
NSMutableArray *s_m_arr = [NSMutableArray arrayWithArray:s_n_array];
NSLog(@"%@,%@",s_n_array,s_m_arr);
//输出
(lldb) print s_n_array
(__NSArrayI *) $0 = 0x00006000027fe7c0 @"2 elements"
(lldb) print s_m_arr
(__NSArrayM *) $1 = 0x000060000298d590 @"2 elements"

那么可变数组开辟了新的内存地址存储当前传递过来的数组,所指向的地址跟不可变数组的地址是不一致的。

如果是这样赋值,那么会发出警告,但是不会崩溃:
NSArray *s_n_array = [NSArray arrayWithObjects:@"n_1",@"n_2", nil];
NSMutableArray *s_m_arr = s_n_array;
NSLog(@"%@,%@",s_n_array,s_m_arr);
这样赋值等同于不可变数组的浅拷贝,如上NSArray *arr_cp = array.copy,所以地址指向都相同;
警告提示指针类型不兼容,也就是说对象不兼容,
Incompatible pointer types initializing 'NSMutableArray *' with an expression of type 'NSArray *'
输出:
(lldb) print s_n_array
(__NSArrayI *) $0 = 0x0000600003dae2c0 @"2 elements"
(lldb) print s_m_arr
(__NSArrayI *) $1 = 0x0000600003dae2c0 @"2 elements"

扩展:

NSArray *arr_o = [NSArray arrayWithObjects:@”1″,@”2″, nil];
NSMutableArray *arr_m = [arr_o mutableCopy];
NSMutableArray *arr_m_1 = [NSMutableArray arrayWithArray:arr_o];
//以上两个可变数组的赋值是等价的。

十七,日常做项目中用到几种锁,场景是什么?

1,@synchronized,保证其内部代码的安全性,也就是原子性,防止self对象在同一时间内被其他线程访问。
缺点:需要消耗大量cpu资源
通常用于单例创建,文件写入等:

+(id)shared
{
    @synchronized(self){
        if(shared == nil){
            shared = [[self alloc]init];
        }
    }
    return sharedManager;
}

2,pthread_mutex 互斥锁,只能有一个线程进行操作,等待期其线程执行,后面的线程需要排队,并且 lock 和 unlock 是对应出现的,同一线程不允许多次pthread_mutex_lock。

    __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);
    }];
//输出
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 

3,NSCondition,状态锁,可让其线程进入等待状态,等待结束后执行后续操作。

    NSCondition *cLock = [NSCondition new];
    //线程1
 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"线程开始");
        //加锁
        [cLock lock];
        //让当前线程等待2秒,然后执行下边的操作
        [cLock waitUntilDate:[NSDate dateWithTimeIntervalSinceNow:2]];
        //等待结束后执行的操作
        NSLog(@"线程1");
        //释放锁
        [cLock unlock];
    });

4,信号量,dispatch_semaphore_t。用来控制访问资源的数量的标识,其主要作用是:
①,保持线程同步,将异步执行任务转换为同步执行任务;
②,保证线程安全,为线程加锁。
dispatch_semaphore_signal,发送信号,信号量+1;
dispatch_semaphore_wait,等待,等到信号量大于0的时候才去执行其下面的任务,或者超时后才回去执行,每执行一次,型号量-1;

    dispatch_semaphore_t _sema = dispatch_semaphore_create(0);
        [NSThread detachNewThreadWithBlock:^{
            for (int i=0; i<10; i++) {
                NSLog(@"111111111111");
            }
            dispatch_semaphore_signal(_sema);
        }];
        [NSThread detachNewThreadWithBlock:^{
            dispatch_semaphore_wait(_sema, DISPATCH_TIME_FOREVER);
            for (int i=0; i<10; i++) {
                NSLog(@"222222222222");
            }
        }];
//线程顺序执行

以下为子线程并发执行,用信号量控制顺序,也就是所谓的任务数:

-(void)dispatchSignal{
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务1:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务2:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务3:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务4:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务5:%@",[NSThread currentThread]);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"任务6:%@",[NSThread currentThread]);
    });
}

十八,多层block如何解决循环引用?

首先,__weak修饰的对象在block中不可以被重新赋值。
其次,Block中如果对一个对象的属性进行引用,那么其实它是对当前对象进行强引用,并不是对象的属性,例如:
self.block = ^{
NSLog(@”%@”,self.student);
});
这里看似引用了对象的student,但是强应用的是self。

避免循环,嵌套__strong协同使用:

- (void)setUpModel{
    XXModel *model = [XXModel new];
    __weak typeof(self) weakSelf = self;
    model.dodoBlock = ^(NSString *title) {
        __strong typeof(self) strongSelf = weakSelf;//第一层
        strongSelf.titleLabel.text = title;
        __weak typeof(self) weakSelf2 = strongSelf;
        strongSelf.model.dodoBlock = ^(NSString *title2) {
            __strong typeof(self) strongSelf2 = weakSelf2;//第二层
            strongSelf2.titleLabel.text = title2;
        };
    };
    self.model = model;
}

strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,不会存在循环引用问题。 如果在Block内需要多次访问 self,则需要使用 strongSelf。
__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用。
出处链接:https://juejin.cn/post/6844903680802553870

十九,alloc和init分别做了哪些事?

alloc做了三件事:
1,cls->instanceSize(extraBytes),计算对象需要多大内存空间;
2,调用calloc函数申请内存并返回内存的指针地址;
3,obj->initInstanceIsa 将 cls类 与 obj指针(即isa)关联。
init做了一件事就是返回当前对象。

Leave a Reply

Required fields are marked *