阅读 6752

iOS开发--我与面试官有个约会

[TOC]

一、写在前面

首先是对帮助过我的大哥大姐们来一波感谢😉在我换工作这段时间里有内推的,答疑解惑的,送吃的甚至还有看风水的(/ω\),多少让Lisa这个小透明有些受宠若惊了。真的非常感谢大家对我的帮助和建议。

对行情的分析

也许是年关将至,身边很多朋友听到我要换工作的消息,大多态度都是酱紫的: “傻狍子,你知道今年多难么” “至少撑到年后啊” 对于这些想法我只能……举双手双jio赞同。但是能怎么办呢,我把公司干倒了┓( ´∀· )┏,不动一动恐怕连馒头皮都吃不起。干巴爹! 这个月也面了大大小小二三十家公司了,就我的观察来说,中型公司岗位确实很少,大公司和外派岗位相对多些,而小公司就算面上了也压工资并且……有些还歧视女生。一上来就问生没生娃啊,能不能接受007。之前也跟几个朋友沟通过,现在也算是入职高峰期,年底要求并不是特别高,很多岗位在一月份就开始停止招聘了。因此个人感觉虽然今年行情不是特别好,但机会还是很多的。

学习内容

这次也是准备得稍微仓促了些,毕竟平常开发以业务为主。除了对自己项目进行总结之外,底层方面的我看了下面的资料:

  • 源码
  • 小码哥明杰老师的底层分析原理
  • 慕课网的全方位剖析iOS高级面试
  • 当然还有我的老东家潭州教育(🤔现在应该是逻辑教育)
  • 小码哥的恋上算法与数据结构
  • 极客时间的https
  • 极客时间戴铭老师的iOS开发高手课
  • 黑马程序员刀哥的三天面试资料(很旧但是很实用的资料)

<( ̄▽ ̄)/讲真,我没看完。 此处绝对没有营销嫌疑,穷得只能啃馒头皮的我常年混迹于N手市场,平常也喜欢搜集一些好的资料。就单纯觉着这些资料还行。

简历

emmm……简历被内推的大佬打回来N次,也参照了很多模板。跟改毕业论文似得。简单来说就不要整那些花里胡哨的排版吧,然后star法则把自己的经历说清楚就好。当然,要对简历上所写的每个字都负责。不然被问到达不出来会有点尴尬啊 ̄□ ̄|| 这是N年前写的一篇关于简历的文章

二、内容

能整理出来的尽量整理,有些博客比我写的好,就直接粘了

1、runtime部分

这真的是……必问题。大概14/15的概率吧。

先粘一篇很nice的博客镇楼。

runtime一篇不错的文章

运行时的体现

OC的动态特性表现为了三个方面:动态类型、动态绑定、动态加载;

  • 动态类型:要等到运行时(run time),即程序运行的时候才会根据语境来识别。动态类型是跟静态类型相对的。像内置的明确的基本类型都属于静态类型(int、NSString等)。静态类型是在编译时期确定。
  • 动态绑定:运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定
  • 动态加载:在运行的时候加载可执行代码和资源比如Retina设备上加载@2x的图片还有整合一些分类

介绍下runtime的内存模型(isa、对象、类、metaclass、结构体的存储信息等)

一个OC对象包含着id和class,id是objc_object类型的结构体,里面是一个isa指针,指向它的类。而class是objc_class的结构体,继承自objc_object,还包含着super class,用来缓存指针和虚函数表的cachebits(bits是可以通过&方法来得到方法列表)

一个对象的isa指针指向哪个class就代表它是哪个类的对象,而对于class来说,它也是一个对象,为了描述class,就产生了metaclass。


消息转发流程

消息发送-》消息动态解析-》消息转发

1、oc的消息机制

oc的一个特性就是在运行时通过发送消息来达到调用函数的目的。在发送消息的时候完整的流程是会经历一下三个阶段的:消息发送、动态消息解析和消息转发。 消息发送:当我们在代码中去调用OC方法时,会转化成一个C++的方法,objc_msgsend,这个方法中第一个参数是reciver(方法接收者),第二个参数是sel_registername,返回的是一个char* 类型的方法名。内部的实现过程是这样的: a、首先判断方法的接收者是不是为nil,若为nil就中止程序 不为nil则会去类的cache中找该方法,cache是一个叫做cache_t的结构体,它里面有一个叫做bucket_t的散装列表、一个mask和一个已经存放的方法的数量,而这中间的bucket_t也是一个结构体,有一个对应方法名的key,一个imp指针,执行函数的方法列表。在cache中找寻方法时,采用了一种时间换空间的方式,传入的方法名&mask,若得到的方法名与bucket_t中的某个key是一样的,就直接调用此方法。 b、若不一样,会接着找到class_rw_t的结构体中寻找一个方法列表。方法名已经排好序则用二分法查找,若无则去遍历数组。找到了之后会先放入到缓存中,然后再去调用该方法,若找不到,会通过super_class指针去其父类中按照上面的步骤查找一遍。直到没有父类则会进入到消息的动态转发机制。

2、消息的动态解析

会去判断是否已经解析过,若没有解析过,才会去根据这个方法是类方法还是实例方法去调用resolveClassMethodresolveInstanceMethod方法。而在这两个方法中开发者就可以写一些比如说class_addMethod的代码去动态添加一些方法。如果程序找到了动态方法,就会讲动态解析标记成yes,再去动态调用这个方法。若没找到动态解析,则会进入到消息转发的过程。

3、消息的转发 一进入会调用forwardingTargetForSelector的方法,如果该方法实现则直接调用,若没实现会调用msgSignatureForSelector做一个方法签名,然后再调用forgetInvocation这样一个方法。如果说这两个方法开发者都没实现,程序将会调用doesNotRecoginize然后报没有找到该方法的错误。


在方法调用的时候,方法查询-> 动态解析-> 消息转发 之前做了什么

检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。


作用
  • method swizzling替换方法(nsarry)
@implementation NSArray (分类)

+ (void)load {
// 写在load方法里,保证调用的时候已经进行了交换
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

- (id)xx_objectAtIndex:(NSUInteger)index {
    // 判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        @try {
            return [self cm_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息。如果是线上,可以在这里将崩溃信息发送到服务器
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } // 如果没有问题,则正常进行方法调用
    else {
        return [self xx_objectAtIndex:index];
    }
}

复制代码
  • 关联给分类添加伪属性 set_AssociatedObejct

关联对象由AssociationsManager管理并在AssociationsHashMap中管理。所有对象的关联内容都在同一全局容器中。 实现:通过关联策略(比如copy、retain等)将它的key(关联值)和(关联值的value)封装成objectAssociation的结构,然后把这个结构和key封装成objectAssociationMap,最后将该map与key结合放入全局的AssociationsHashMap

当setObject传成nil的时候,就能清除关联对象。 当policy为OBJC_ASSOCIATION_ASSIGN的时候,设置的关联值将是以weak的方式进行内存管理的。

  • 字典转模型get_classProptyList/kvc
#import "NSObject+runtime.h"
#import <objc/runtime.h>
@implementation NSObject (runtime)
+ (NSArray *)llxGetProperties  {
    unsigned int count = 0;
    objc_property_t *propers = class_copyPropertyList([self class], &count);
    NSMutableArray *arr = [NSMutableArray array];
    for (int i = 0 ; i < count; i++) {
        objc_property_t pty = propers[i]; // 结构体指针不需要*
        const char *cname = property_getName(pty);
        NSString *name = [NSString stringWithCString:cname encoding:(NSUTF8StringEncoding)];
        [arr addObject:name];
    }
    return arr;
    
}
@end
复制代码
  • 多播代理

实现一个管理类,将需要回调的对象注册进来,然后将事件消息发送给这个管理类,由于这个管理类是没有实现委托方法的,就不能正常处理这个消息,这个时候就会走消息转发流程;然后我们通过消息转发流程,将消息转发到注册进来的对象中


runtime根元类的isa指针指向谁,根元类的父类指向

根元类的isa指向它自己,父类是指向NSObject


hook和AOP

说白了,AOP编程就是在运行时动态的将代码切入到类的指定方法、指定位置上的编程方式。 ObjC中的AOP--面向切面编程


forgetTargetForSelector方法究竟做了什么事

把消息的实现转给别的接收者去处理。比如说一个person类,调用了run方法,但是它没有实现。最后会走到forgetTargetForSelector。此时car类有run方法,我们直接返回car类,这样就会调用car类的方法。


重写了forgetTargetForSelector会有什么风险?
method swizzling你应该注意的点

method swizzling你应该注意的点

forwardingTargetForSelector同为消息转发,但在实践层面上有什么区别

forwardingTargetForSelector仅支持一个对象的返回,也就是说消息只能被转发给一个对象

forwardInvocation可以将消息同时转发给任意多个对象


2、runloop部分

一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其实内部就是do-while循环,这个循环内部不断地处理各种任务(比 如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。 在 CoreFoundation 里面关于 RunLoop 有5个类:CFRunLoopRefCFRunLoopModeRefCFRunLoopSourceRefCFRunLoopTimerRefCFRunLoopObserverRef其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

runloop流程


runloop的作用
  • NSTimer切换模型,使其在滑动页面时也能计时
  • 线程保活

当经常要在子线程中做一些操作时,可以让子线程一直存活,这样可以减少性能的消耗。在线程的[nsthread alloc]initwithblock方法中加入一个runloop保持子线程的生命,在别的地方通过performforselector去做线程想要做的事情

  • 卡顿检测

runloop监控卡顿

todo:这块到时候会抽个demo出来 监控卡顿其实就是找到主线程中都做了什么事情。如果 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤 醒后接收消息时间过长而无法进入下一步的话,就可以认为是线程受阻了。kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting

创建一个 CFRunLoopObserverContext 观察者; 将创建好的观察者 runLoopObserver 添加到主线程 RunLoop 的 common 模式下观察; 创建一个持续的子线程专门用来监控主线程的 RunLoop 状态; 一旦发现进入睡眠前的 kCFRunLoopBeforeSources 状态,或者唤醒后的状态 kCFRunLoopAfterWaiting,在设置的时间阈值内一直没有变化,即可判定为卡顿; dump 出堆栈的信息,从而进一步分析出具体是哪个方法的执行时间过长。

总结一下:思路就是监听runloop在主线程中做的哪些事。根据runloop的流程可以得知,我们可以创建一个观察者放到runloop的主线程当中去,当beforesource到afterWating的阶段大于某个阈值时,我们就认为产生了卡顿。 具体实现,创建一个监控的单例类,该类提供一个监控的方法。在这个监控方法中首先要做的是创建一个观察者,把它放入到主线程,再就是开辟一个子线程去观察在规定时间内runloop状态的改变。观察者是用cfrunloopobservecreate来创建的,我们设置监听它的所有属性,该方法还有一个传入函数指针的参数,那我们就可以传入一个方法,这个方法就是监控runloop状态的改变。在该方法中用一个bool类型的变量isbegin=yes来表示进入休眠,并且记录开始休眠的时间,到唤醒的时候把isbegin改成no。 接下来用一个已经保活的子线程去触发监听的方法,监听方法里创建timer,把timer添加到子线程对应的runloop中,每隔0.01秒去监控一次,如果begin=yes,并且当前时间与休眠之前时间差值大于阈值,就表示卡顿了。


3、OC基础

load、initialize方法的区别什么?在继承关系中他们有什么区别?

load是在main函数之前装载文件时被调用的,只被调用一次,而且它不是采用objc_msgSend方式调用,而是直接采用函数的内存地址调用。多个类的load跟complile source中的文件有关,根据文件从上而下调用,子类和父类同时实现load方法时,父类优先子类。分类和本类都有load方法,优先本类

initialize:当类或子类第一次收到消息时被调用只调用一次 调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕 子类和父类同时实现initialize,父类的先被调用,然后调用子类的 本类与category同时实现initialize,category会覆盖本类的方法,只调用category的


iOS中NSSet 和 NSArray的区别

NSSet是无序集合,在内存中是不连续的,集合中的元素不可重复。内部有hash算法,因此查找效率更高。应用场景有配合NSArry做去重处理,cell的重用机制是随机在重用池中获取一个cell NSArray是有序集合,在内存中连续,应用场景是tableview的数据源的数据要按照其下标取值。


ios 同时重写setter和getter方法

系统会报这个错误 Use of undeclared identifier '_name';did you mean 'name' 解决方法是 @synthesize wtName = _wtName; 原因:用@property声明的成员属性,相当于自动生成了setter getter方法,如果重写了set和get方法,与@property声明的成员属性就不是一个成员属性了,是另外一个实例变量,而这个实例变量需要手动声明。所以会报错误。@synthesize 声明的属性=变量。意思是,将属性的setter,getter方法,作用于这个变量


iOS通用属性关键字
  • 读写权限

readOnly readWrite

  • 原子性

atomic(默认):保证对成员属性的赋值和获取是线程安全的(不包括操作。例如对一个被atomic修饰的数组进行删除或添加是不在atomic作用范围内的) noatomic

  • 引用计数

retain/strong:retain通常在mrc中使用。这两个关键字都是用来修饰对象的 assgin/unsafe_unretain: assgin既可以用于修饰基础数据类型也可以修饰对象。在修饰对象类型时是不会改变引用计数的。assgin指针仍然指向原地址,此时若assgin指针仍去访问原地址,则可能由于悬垂指针而导致内存泄露。unsafe_unretain只有在mrc中使用比较频繁 copy weak


属性修饰符atomic的内部实现是怎么样的?能保证线程安全吗

atomic表示的是原子性。OS 10之后,苹果弃用了 OSSpinLock 改为新的 os_unfair_lock。声明属性时,编译器会自动生成getter/setter方法,最终调用objc_getPropertyobjc_setProperty方法来进行属性的存取。此时使用atomic,这两个方法内部会用os_unfair_lock进行加锁,来保证读写的原子性。锁都在propertylocks里保存着,在用之前会把锁初始化好,要用的时候通过对象地址加上成员变量的偏移量为key,去propertylocks取。存取时用的是同一个锁,所以atomic能保证属性的存取时是线程安全的。

atomic为什么不能保证绝对的线程安全? atomic在getter/setter方法中加锁,仅保证了存取时的线程安全,假设我们的属性是@property(atomic)NSMutableArray *array;可变的容器时,无法保证对容器的修改是线程安全的 在编译器自动生产的getter/setter方法,最终会调用objc_getPropertyobjc_setProperty方法存取属性,在此方法内部保证了读写时的线程安全的,当我们重写getter/setter方法时,就只能依靠自己在getter/setter中保证线程安全


iOS 中内省的几个方法有哪些?内部实现原理是什么

对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。 -(BOOL) isKindOfClass: 判断是否是这个类或者这个类的子类的实例 -(BOOL) isMemberOfClass:判断是否是这个类的实例 -(BOOL) respondsToSelector: 判断实例是否有这样方法 +(BOOL) instancesRespondToSelector: 判断类是否有这个方法 反射就是利用字符串去动态的检测,从而实现运行时的转化。


assgin和weak有何区别?

weak用于修饰对象,而assgin既可以修饰基本数据类型也可以修饰对象 assgin修饰的对象被释放之后,assgin指针仍指向原对象的地址,而weak在被修饰的对象释放之后会自动置为nil


@property(copy)NSMutableArray *array这句代码会产生什么问题?

浅拷贝:让目标对象指针和源对象指针都指向同一内存空间。所以它会增加被拷贝对象的引用计数。 深拷贝:让目标对象指针和源对象指针分别指向内容相同的两块内存空间。因此不会增加被拷贝对象的引用计数。 由上图可以看出,对于可变对象的copy和mutableCopy都是深拷贝。不可变对象的copy是浅拷贝,mutableCopy是深拷贝。copy方法返回的都是不可变对象。 copy关键字返回的目标对象都是不可变的。而源对象被声明为可变数组类型,就会有调用方对其进行添加或移除数据的操作。而此时被拷贝的结果是一个不可变对象,就容易引起某些程序异常的问题


屏幕成像原理

原理:CPU计算好屏幕要显示的内容(如视图创建,布局计算,图片解码,文本绘制等)放入缓存中,GPU从缓存中读取数据等待显示器的垂直同步信号(V-Sync)发出进行渲染,再将结果放入到帧缓冲区,随后视频控制器读取帧缓冲区的数据,将其转换并传递给显示器显示。 注:iOS是双缓冲区


事件传递机制

当用户触摸屏幕时,iOS系统会将事件加入到UIApplication管理的一个任务队列中,UIApplication会将处于任务最前端的事件分发到UIWindow中,继而分发到UIView中。UIView会查看自身是否能处理事件,触摸点是否自己身上。若能则以相同的方式遍历子控件。若在子控件中没有找到,则自身就是事件处理者。若自身不能处理,则不做任何操作。

UIView不能接受事件处理的情况有如下三种:

  • alpha < 0.01
  • userInteractionEnable = NO
  • hidden = YES

如何寻找最适合的view?
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event

// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

复制代码

事件传递给控件后,就调用hitTest方法寻找更合适的控件,如果子控件是合适,则在子控件中再调用上述方法,直到找到最合适的控件或废弃事件。当父控件不可用时,返回nil,子控件也将无法继续寻找合适的view。


如何扩大button的点击范围

创建UIButton的分类重写方法

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect bounds = self.bounds;
CGFloat widthDelta = 44.0 - bounds.size.width;
CGFloat heightDelta = 44.0 - bounds.size.height;
bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);
return CGRectContainsPoint(bounds, point);
}
复制代码

setNeedsLayout、layoutIfNeed区别

setNeedsDisplay会调用drawRect方法重画页面

setNeedsLayout页面不会发生变化,只有通过调用layoutIfNeed调用layoutSubviews,页面才会发生变化。

什么情况下会调用layoutSubviews ?

一篇博客

  1、调用setNeedsLayout layoutIfNeed,直接调用setLayoutSubviews

  2、addsubview时触发layoutSubviews

  3、改变一个view的frame会触发layoutSubviews

  4、改变view的size会触发父view的layoutSubviews

  5、滚动会触发layoutSubviews

  6、旋转Screen会触发父UIView上的layoutSubviews事件

什么情况会调用draw rect方法

  1、controller的loadView、viewdidLoad方法调用之后,view即将出现在屏幕之前系统调用drawRect。

  2、sizeToFit方法调用之后。

  3、设置contetMode为UIViewContentModelRedraw,之后每次更改frame的时候调用redraw方法。

  4、调用setNeedsDisplay方法。


数据源同步问题

来看这样一个场景,在列表要发生变化时,会在主线程中做数据拷贝,子线程使用拷贝的数据做网络请求、数据解析或预排版等操作。但如果此时用户做了删除操作,主线程的UI就会刷新。而子线程使用的仍是删除操作之前的数据,因此它返回给主线程的数据就会异常。 诸如此类的现象都涉及到了多线程对共享数据的访问,因此要考虑数据源同步的问题。

  • 方案一:并发访问,数据拷贝

可以在做删除数据时,在主线程中记录下用户的操作。在子线程返回数据之前同步删除操作,再回到主线程中刷新UI。

  • 方案二:串行访问

在子线程中做网络请求等操作,把这些操作放入到串行队列中。若此时用户删除了某行数据,主线程会等待队列中的任务完成后再做数据删除,最后回到主线程中刷新UI。


KVC的原理

键值编码,使用表示属性的字符串来间接访问对象属性值的一种结构。

KVC取值的实现:按照getKey、key、isKey的顺序搜索这三个方法,若某一方法被实现,取到的即是方法返回的值。后面的方法不再运行。若没有找到则会调用accessInstanceVariablesDirectly方法判断是否允许取成员变量的值。如果返回NO,会直接调用valueForUndefinedKey方法、若返回yes,会按先后顺序取_key、_isKey、key、isKey的值。若都没取到,还是会调用valueForUndefineKey方法

KVC设置的实现:首先搜索是否有setKey方法,如果没有会调用accessInstanceVariablesDirectly方法判断是否能直接访问成员变量。若不能则调用setValueForUndefinedKey方法,抛出异常。若返回yes,则按照_key、_isKey、key、isKey的顺序搜索成员名。如果还未搜到同样会调用setValueForUndefinedKey方法。


KVC是否违背了面向对象的编程思想

-(id)valueForKey:(NSString *)key-(void)setValue:(id)value forKey:(NSString *)key方法中的key都是没有限制的。也就是说,只要外界知道key的名字,就可以获取或者访问它。因此从这个角度来说,kvc是有违背面向对象的思想的。


KVO原理部分

假设有一个类A。当外界调用addObeserver方法时,系统会在动态运行时创建NSKVONotifying_A的类继承于原来的类,同时将类A的isa指针指向新创建的类。在新创建的这个类中,重写Setter方法,以达到通知所有观察对象的目的。


-(void)setValue:(id)obj {
    [self willChangeValueForKey:@"keyPath"];
    [super setValue:obj];
    [self didChangeValueForKey:@"keyPath"];
}
复制代码

问:如何手动触发kvo?

答:在对成员变量赋值的时候,可以在赋值前调用willChangeValueForKey方法,而在赋值之后调用didChangeValueForKey方法。didChangeValueForKey在系统内部实现当中会触发KVO回调。


UIView和CALayer

UIView可以看做是CALayer的管理者,它为CALayer提供内容,还可以处理触摸等事件,参与响应链。而CALayer继承自NSObject,无法响应事件,只负责显示内容。 UIView中的layer属性可以返回它的主图层的实例,该属性的内部,维护着三份拷贝,即逻辑树,动画树(进行各种渲染操作)和显示树(显示屏幕上的内容)。 UIView拥有的layerClass方法,可以返回主图层所使用的类。UIView的子类,通过重载该方法,可以实现使用不同的CALayer来显示内容。

为啥UIView负责提供内容,而CALayer负责显示?

答:单一职责。


wkwebview的cookie问题

这块要看个人项目了,我们还有一个操作就是第一次打开webview的时候,把token存入了cookie中,前端同学也会做一系列操作。比如说当token失效的时候,会主动与原生的桥进行通信,原生端收到消息后跳转到登录页面。推荐一个还不错的处理cookie的三方库叫做GGCookie

4、优化操作

分为卡顿优化、电池、内存优化、安装包瘦身、冷启动优化。 在规定的16.7ms之内,下一帧VSync信号到来之前,并没有CPU和GPU共同完成一帧画面的合成,则这一帧就会被丢弃掉。可以通过core Animation查看FPS值。

  • 图层混合,指的是多个UI控件叠加,有透明或者半透明的控件,那么GPU就会去计算这些layer最终显示的颜色,那这个过程就会消耗性能。通过模拟器Debug->Color Blended Layers查看混合图层。红色为混合图层。

比如说UIlabel显示中文时,由于实际渲染的区域大于label的size,最外层多了一个sublayer,如果不设置label.layer.masksToBounds = YES,那第二行的label边缘就会出现红色。

  • 离屏渲染

屏幕外创建新缓冲区,离屏渲染结束后,再从离屏切到当前屏幕, 把离屏的渲染结果显示到当前屏幕上,这个上下文切换的过程是非常消耗性能的。 (1)drawRect:方法 (2)layer.shadow (3)layer.allowsGroupOpacity or layer.allowsEdgeAntialiasing (4)layer.shouldRasterize (5)layer.mask (6)layer.masksToBounds && layer.cornerRadius

tableview的优化

1、自定义cell的光栅化:光栅化将cell的所有内容都生成一张位图。这会对性能有很大的提升。但是如果对做光栅化layer再去频繁绘制,就会导致离屏渲染。比如cell从复用池中去拿到数据,会对cell的内容进行重绘。因此还需要加上异步绘制这句话。

self.layer.shouldResteraize = yes;
self.layer.scale = [UIScreen mainScreen].scale;//默认分辨率x1
self.layer.drawsAsynchronously = yes; // 异步绘制
复制代码

2、行高一定要缓存 UITableview是继承自uiscrollview的,它会先去计算布局和位置,再将cell放入到对应的位置当中。滚动或者刷新的时候,其实是要调用很多次heightforRow方法的。如果在这里要不停的计算高度并且返回会非常耗时。可以选择做行高的缓存来避免。 在model中创建一个cellHight的属性,在获取数据源的时候就可以通过文字的size和字体去计算出行高来。当调用heighforrow这个代理方法时,直接取出数据源的model赋值即可。 在model里面的.m文件的-cellHight方法:CGRectMake去拿到每个组件的size,通过cgrectgetMaxY拿到上一个显示内容的高度。对于文本的计算就是把字体的大小传入富文本,然后使用boundingRectWithSize计算出矩形空间,从而得到高度

 _cellHeight = CGRectGetMaxY(self.pictureFrame);
复制代码
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    DetaileModel *model = _listArray[indexPath.row];//取出模型
    return model.cellHeight;//获得所需高度并返回
}
复制代码

3、不要动态的创建子视图:所有的子视图都预先创建,再通过hidden的形式去控制显示。比如说类似于朋友圈九宫格图片,通过collectionView创建的话cell的内容会变得很庞大,而且最多也只能显示九张图片,所以可以在创建cell的时候就创建9个imageView即可.在使用或者复用的时候,根据图片个数或者展示需求来保证其显示或者隐,然后调整好约束就好了。

4、子视图设置背景颜色。A做push动作到B.如果B没有设置背景色的话,会出现一个明显的卡顿

5、尽量不要使用alpha设置透明度。它会让GPU去计算最终显示的颜色。

6、runloop滚动模式下不渲染图片,在休眠的时候渲染


内存

使用instrument leaks检测,打开product->profile->leaks,点击 callTree,在下方筛选定位泄露的代码进行修改。


耗电优化

耗电的主要来源为:CPU 处理;网络请求;定位;图像渲染;

  • 尽可能降低 CPU、GPU 功耗;
  • 少用定时器;
  • 优化 I/O 操作;

尽量不要频繁写入小数据,最好一次性批量写入; 读写大量重要数据时,可以用 dispatch_io,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;数据量大时,用数据库管理数据;

  • 网络优化;

减少、压缩网络数据(JSON 比 XML 文件性能更高); 若多次网络请求结果相同,尽量使用缓存; 使用断点续传,否则网络不稳定时可能多次传输相同的内容; 网络不可用时,不进行网络请求; 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间; 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;

  • 定位优化;

如果只是需要快速确定用户位置,用 CLLocationManager 的 requestLocation 方法定位,定位完成后,定位硬件会自动断电; 若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务; 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest; 需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically 为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;


启动优化

冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。

第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。

  • dyld

dyld(Dynamic Link Editor),Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)。启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。

  • Runtime

启动 App 时,调用 map_images 进行可执行文件的内容解析和处理,再 load_images 中调用 call_load_methods 调用所有 Class 和 Category 的 load 方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor)) 修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。

  • main

在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions: 函数。

针对不同的阶段,有不同的优化思路:

减少动态库、合并动态库,定期清理不必要的动态库; 减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类; 减少 C++ 虚函数数量; Swift 开发尽量使用 struct; 用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))、C++ 静态构造器、以及 Objective-C 中的 load 方法; 将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中; 总结得比较好的文章


5、防止crash

调用了未实现的实例方法,有三次转发机会可以处理,如果这三次都没处理,才会崩溃在doesNotRecognizeSelector:中,所以我们可以利用这个原理来避免崩溃。这三次转发:resolveInstanceMethod:、forwardingTargetForSelector:和methodSignatureForSelector:+forwardInvocation:,其中最适合用来处理的可能就是第三次转发了,因为我们会用分类的方式重写方法,尽量在消息转发越往后的阶段影响越小。因为使用分类重写,要注意工程是否有其他类也重写了这两个方法。

unrecognized selector sent to instance
@implementation NSObject (MethodForward)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSString *string = [NSString stringWithFormat:@"[%@ %@]",[self class],NSStringFromSelector(aSelector)];
    [[MethodForwardManager sharedInstance] recordMethodForward:string];

    NSMethodSignature *signature = [MethodForwardManager instanceMethodSignatureForSelector:@selector(recordMethodForward:)];
    return signature;
}

//需要重写这个方法才会避免崩溃
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
}
@end
复制代码

调用类方法我们利用的是转发的第一步:添加动态方法。因为这一步有专门针对类方法的处理resolveClassMethod:。

KVC造成的crash

[object setValue:nil forKey:key] value为nil,key不为nil的时候会调用-(void)setNilValueForKey:(NSString *)key这个方法,可以对这个方法进行重写,代码如下:

-(void)setNilValueForKey:(NSString *)key{
   NSString *crashMessages = [NSString stringWithFormat:@"JKCrashProtect:'NSInvalidArgumentException', reason: '[%@ %p setNilValueForKey]: could not set nil as the value for the key %@.'",NSStringFromClass([self class]),self,key];
   [[JKCrashProtect new] JKCrashProtectCollectCrashMessages:crashMessages];
}
复制代码
KVO的防护

有个简单的方法:给NSObject 增加一个分类,然后利用Run time 交换系统的 removeObserver方法,在里面添加 @try @catch。

// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
    @try {
        [self removeDasen:observer forKeyPath:keyPath];
    } @catch (NSException *exception) {}
}

复制代码
NSNotificationCenter指定线程接收通知

6、block部分

什么是block?

block是将函数及其上下文封装起来的对象

block指向成一个名为__方法名_block_impl_0的结构体。并传入了参数,函数指针,关于block的描述,局部变量。最终强转为一个函数指针。 在这个名为xxx_block_impl_0的结构体中,包含了以下内容:

struct xxx_block_impl_0 {
    struct __block_impl impl;
    struct xxx_block_desc_0* Desc; //对block的描述
    int multiplier;
    __xxxBlock__method_block_impl_0(void *fp,struct __xxBlock_block_desc——0 *desc,int _xx,int flags=0):multiplier(mutipier) {
        // ……构造函数赋值
    }
}
复制代码

__block_impl也是一个结构体,内容如下:

struct __block_impl {
    void *isa; //说明是对象类型
    int flags;
    int Reserved;
    void *FuncPtr; //花括号要执行的内容
}
复制代码

block的截获
  • 基本数据变量截获其值,也就是做了个赋值操作。使用的是block结构体中截获的变量。
  • 对象数据类型连同修饰符一起截获。
  • 以指针形式截获静态局部变量。在block中对静态局部变量进行修改,用的是新的值
  • 不截获全局变量和静态全局变量。

__block

一般情况下,对截获变量进行赋值操作的时候需要加上__block修饰符。注意使用不是赋值。对静态变量、静态全局或全局变量,不需要__block赋值。加了__block修饰的变量变成了对象。通过变量的forwarding指针去找到对应的对象,对其进行赋值。

{
  NSMutableArray *array = nil;
  void(^Block)(void) = ^{
    array = [NSMutableArray array];
  };
  Bkock();
}
复制代码

要在array的声明处添加__block修饰符


Block的内存管理

全局:NSConcreateGlobalBlock 栈:NSConcreateStackBlock 堆:NSConcreateMallocBlock

如果对__block变量不进行拷贝,操作的就是栈上的__block变量,如果发生了拷贝,无论操作栈还是堆上的__block变量,都是使用了堆上的__block变量。

7、多线程的问题

GCD定时器

GCD。dispatch_source_set_timer设置倒计时,dispatch_source_set_event_handler设置倒计时执行的任务。

- (void)setupGCD {

    self.bottomLabel.text = @"60";
    __block NSInteger bottomCount = 61;
    
    //获取全局队列
    dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //创建一个定时器,并将定时器的任务交给全局队列执行
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, global);
    
    // 设置触发的间隔时间 1.0秒执行一次 0秒误差
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

    __weak typeof(self)weakSelf = self;

    dispatch_source_set_event_handler(timer, ^{

        if (bottomCount <= 0) {
            //关闭定时器
            dispatch_source_cancel(timer);
        }else {
            bottomCount -= 1;
            dispatch_async(dispatch_get_main_queue(), ^{
                weakSelf.bottomLabel.text = [NSString stringWithFormat:@"%ld",bottomCount];
            });
        }
    });
    
    dispatch_resume(timer);
}
复制代码

平常是如何使用多线程的

举个🌰:使用collectionview,创建轮播图实现左右拖拽时,需要在数据源方法中乘以2倍的图片数据。但是,左右拖拽的代码在init里面会先被执行,因此利用dispatch_async在主队列中异步,保证数据源方法执行完毕后,再滚动collectionView。


栅栏实现多读单写

读写数据库的时候,要满足读者和读者不互斥,写者和写者互斥,读者和写者要互斥,因此需要把写者隔开。

- (id)objectForKey:(NSString *)key
{
    __block id obj;
    // 同步读取指定数据
    dispatch_sync(concurrent_queue, ^{
        obj = [userCenterDic objectForKey:key];
    });
    
    return obj;
}

- (void)setObject:(id)obj forKey:(NSString *)key
{
    // 异步栅栏调用设置数据
    dispatch_barrier_async(concurrent_queue, ^{
        [userCenterDic setObject:obj forKey:key];
    });
}
复制代码

dispatch_group_async组

有ABCD件事情并发执行,完成之后再通知到E

- (id)init
{
    self = [super init];
    if (self) {
        // 创建并发队列
        concurrent_queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
        arrayURLs = [NSMutableArray array];
    }

    return self;
}

- (void)handle
{
    // 创建一个group
    dispatch_group_t group = dispatch_group_create();
    
    // for循环遍历各个元素执行操作
    for (NSURL *url in arrayURLs) {
        
        // 异步组分派到并发队列当中
        dispatch_group_async(group, concurrent_queue, ^{
            
            //根据url去下载图片
            
            NSLog(@"url is %@", url);
        });
    }
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        // 当添加到组中的所有任务执行完成之后会调用该Block
        NSLog(@"所有图片已全部下载完成");
    });
}
复制代码

NSOperation

NSOperation需要配合NSOperationQueue实现多线程编程

  • 具体步骤

先将需要执行的操作封装到一个NSOperation对象中 然后将NSOperation对象添加到NSOperationQueue中 系统会自动将NSOperationQueue中的NSOperation取出来 将取出的NSOperation封装的操作放到一条新线程中执行

  • 优势

可以设定最大并发数

-(NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
复制代码

队列的取消、暂停、恢复

- (void)cancelAllOperations;
// 提示:也可以调用NSOperation的- (void)cancel方法取消单个操作(在内存中给删除了)
 
// 暂停和恢复队列
- (void)setSuspended:(BOOL)b;  YES代表暂停队列,再设置为NO代表恢复之前的队列
- (BOOL)isSuspended;
复制代码

设定操作依赖 NSOperation之间可以设置依赖来规定先后顺序。比如一定要让操作A先执行完后才能执行B

[operationB addDependency:operationA];
复制代码
多线程的各种笔试题

8、内存管理机制

weak的实现原理?SideTable的结构是什么样的

weak是Runtime维护了一个hash(哈希)表,用于存储指向某个对象的所有weak指针。在添加弱引用变量时,最终会调用weak_register_no_lock(),它根据hash算法进行查找,若查找的位置已经有了当前对象对应的弱引用数组,就把新的变量添加到该数组中,若无对应的弱引用数组,则重新创建弱引用数组,在第0个位置加上该变量,其他位置初始化为0。 在对象调用dealloc的时候,内部会最终会调用weak_clear_no_lock方法,它会更加当前指针查找弱引用表,把当前对象对应的弱引用都拿出来得到一个数组,遍历该数组的所有弱引用指针,分别置为nil.

SideTable包含了自旋锁、弱引用表和引用计数表。自旋锁是一种忙等锁,适用于轻量访问


Autoreleasepool的原理?所使用的的数据结构是什么?
如果不是通过alloc、new、retain、copy 创建出来的对象都是autorelease对象。比如:"NSString *str = [NSString stringWithFormat:@"xiaoyueyue"],类方法创建的对象str为autorelease对象。
如果是通过alloc、new、retain、copy 创建出来的对象怎么进行处理?(通过arc内部进行引用计数来进行管理的
复制代码

自动释放池是以栈为节点通过双向链表组合而成的一种结构。当每次runloop将要结束的时候会调用autoreleasepoolPage::pop(),同时会push一个新的释放池。(双向链表:头节点的父指针指向空,后续的每个节点子节点指向后一个节点,父指针指向前一个节点,最后一个节点的子指针指向一个空节点。栈:向下增长,地址由低到高。后入先出) 场景:比如for循环alloc图片等内存消耗比较大的数据,需要插入,防止当时内存消耗过大 原理: 编译器会将@autoreleasepool{}改写成

void *ctx = objc_autoreleasePoolPush();
{}中的代码
objc_aotureleasepoolpop(ctx)
复制代码

objc_autoreleasePoolPush会调用C++中AutoreleasePoolpage中的push方法,而objc_aotureleasepoolpop则会调用AutoreleasePoolpage的pop方法。AutoreleasePoolpage包含了一个next指针,一个child,一个parent指针、还有一个对应的线程。 当做一次push操作的时候,会把next指向的位置置为nil,将next指向下一个可入栈的位置。如果说next的位置指向的是栈顶,则会开辟一个新的栈来操作。 当做一次pop操作时,会根据传入的哨兵对象找到对于的位置,给上次push操作后添加的对象发送release消息。然后回退Next到正确的位置。


ARC的实现原理?ARC下对retain & release做了哪些优化

ARC是由LLVM(编译器)和runtime协作的结果。ARC中禁止手动调用retain/realse/retainCount/dealloc(可重写delloc但不能显式调用)方法。当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。ARC中新增了weak/strong关键字属性关键字。


9、分类和扩展

源码下载:runtime开放源码

在项目中用分类做过什么?
  • 可以给某个类扩充方法,从而分解体积庞大的类文件
  • 声明私有方法:如果某些类的方法不想公开,但又出于某种原因需要被别的类调用该类中的某些方法。可以新建该类的分类,在分类中声明这些方法。在调用的类中,只需要引入分类即可。
  • 把framework的方法公开化。给系统自带的类添加分类

分类的原理是什么

在remethodizeClass类中,调用attachCategories方法。

static void 
attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;
    if (PrintReplacedMethods) printReplacements(cls, cats);

    bool isMeta = cls->isMetaClass();

    // fixme rearrange to remove these intermediate allocations
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    // Count backwards through cats to get newest categories first
    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];

        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();

    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
复制代码

cats是装所有分类的数组,在这段方法中,采用倒序遍历。mlist是二维数组,装载分类的方法,属性和协议列表。rw->methods.attachLists(mlists,mcount)表示将二维方法列表添加到原来宿主类的列表中。属性列表与协议列表的添加原理亦是如此。

这段引自:iOS底层原理 - category的本质以及源码分析 通过以上分析可以得出Category的加载处理过程: 首先由runtime加载某个类的所有分类数据,将分类中的方法、属性、协议数据都合并到一个大数组中。而由于是倒序的方式遍历,所以后面参与编译的Category数据会在数组的前面。最后将合并后的分类数据插入到类原来数据的前面。 此处引用:深入理解Objective-C:Category 对于添加类的实例方法而言,又会去调用attachCategoryMethods这个方法

for (uint32_t m = 0;
             (scanForCustomRR || scanForCustomAWZ)  &&  m < mlist->count;
             m++)
        {
            SEL sel = method_list_nth(mlist, m)->name;
            if (scanForCustomRR  &&  isRRSelector(sel)) {
                cls->setHasCustomRR();
                scanForCustomRR = false;
            } else if (scanForCustomAWZ  &&  isAWZSelector(sel)) {
                cls->setHasCustomAWZ();
                scanForCustomAWZ = false;
            }
        }

        // Fill method list array
        newLists[newCount++] = mlist;
    .
    .
    .

    // Copy old methods to the method list array
    for (i = 0; i < oldCount; i++) {
        newLists[newCount++] = oldLists[i];
    }
复制代码

attachCategoryMethods做的工作相对比较简单,它只是把所有category的实例方法列表拼成了一个大的实例方法列表,然后转交给了attachMethodLists方法。


扩展的作用和特点

作用

  • 声明私有属性(可不对子类暴露)
  • 声明私有方法
  • 声明私有成员变量

特点

  • 编译时决议
  • 只以声明的形式存在,多数情况下寄生于宿主类中的.m文件中
  • 不能为系统类添加扩展

分类和扩展的区别

分类是运行时决议,而扩展是编译时决议(因此扩展可以直接添加成员变量,而分类要通过关联对象的形式) 分类可声明可实现,而扩展只有声明。它的实现要在宿主类中。 可以为系统类添加分类,但不能为系统类添加扩展。


分类可以添加的内容
  • 实例方法
  • 类方法
  • 协议
  • 属性:注意这里说的添加属性只是添加了get方法和set方法,而非实例变量。(当然类别也可以通过关联对象来添加实例变量)。

为什么分类方法会覆盖宿主类的方法?

其实宿主类的方法还是存在的。只是在做内存拷贝的过程中,分类的方法会位于方法列表靠前的位置,由于消息动态转发机制获取到相匹配的方法名就不会再往下寻找。因此,在代码执行的时候只会调用分类的方法。


分类关联对象

问:是否能给分类添加”成员变量“?如果能,那么这个变量被放到了哪里?如何做销毁操作呢?

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self,
                             "name",
                             name,
                             OBJC_ASSOCIATION_COPY);
}
 
- (NSString*)name
{
    NSString *nameObject = objc_getAssociatedObject(self, "name");
    return nameObject;
}
复制代码

答:答案是肯定的。诚然,在分类声明或定义之处是不能给分类添加成员变量的(因为一个类的内存大小是固定的,在其load方法执行前就已经加载在内存之中)。but(emm...大佬都喜欢转折😏)可以通过关联对象的方式为分类添加成员变量。关联对象由AssociationsManager管理并在AssociationsHashMap中存储。所有对象的关联内容都被放在一个统一的全局容器中。

销毁时,runtime的销毁对象函数objc_destructInstance会判断是否有关联对象,若有则会调用_object_remove_assocations做关联对象的清理工作。

关联对象中有一个objcAssociation的对象,这里面包含了协议政策和对象。将objcAssociation作为value传递给objectAssociationMap。key储存着这个对象的指针地址,k-v组成一个map,而这个map又会作为value传给AssociationsHashMapAssociationsHashMap存储着所有的关联对象。


10、算法

只挑出了常问的,链表反转几乎是必问的了。

反转链表

leetcode之两种方法反转链表 头插法思路:定义一个新的newHead初始化null, 节点node为中转站,一开始指向原函数链表的头指针Head。对原链表进行循环遍历,先对原链表做头删操作,再对新链表做头插操作,直到当前head的指针为null。返回newHead.

struct ListNode {
	int var;
	struct ListNode *next
}

struct ListNode *reverseList (struct ListNode *head) {
	struct ListNode *newHead = NULL;
	struct ListNode *node;
	while (head != NULL) {
		node = head;
		head = head->next;
		node->next = newHead;
		newHead = node;
	}
	return newHead;
}

复制代码

递归:A,B,C,D 把BCD看成一个整体,将整体的next指针指向A,内部亦是如此循环

struct ListNode *reverseList2(struct ListNode *head) {
	if(head == NULL || head->next == NULL) {
	return head;
	}else {
		struct ListNode *sonList = reverseList2(head->next);
		head->next->next = head;
		head->next = NULL;
		return sonList;
	}
}
复制代码

快速排序

思路:在要排序的数字当中选一个基准数,存入temp,通常是选第一个数字。将小于基准数的数字移到基准数左边,大于它的移动到右边。对于基准数两边的数组,不断重复以上过程。直到每个子集只有一个元素。即为全部有序。

定义两个指针i,j。i指向头部,j指向尾部。i从左往右移动,j从右往左移动

当i < j的时候,从j开始与刚存放在temp的数字进行比较,如果大于temp则略过,j继续往左移动。知道遇到小于temp的数字。将arr[j]的数字填入到arr[i]中。然后开始移动i指针,当i指向小于基准数的时候略过,i继续向右移动。当它有值大于temp的时候,将arr[i]填入arr[j]中,继续轮换移动两个指针。直到i和j相遇,此时将temp的填入到arr[i]中。

//要是从左往右开始找,当i停在了比基准值大的位置上,与j相遇,将这个值与基准值交换的话,就不符合条件了, //也可能会出现i=j,且a[i]与a[j]的值都比基准值大

void quickSort(int *arr,int begin,int end) {
	if (begin > end) {return;}

	int i = begin;
	int j = end;
	int temp = arr[i];
	while (i != j) {
		while(i<j && arr[j] > temp) j—;
		arr[i] = arr[j];
		while(i<j && arr[i] <= temp)i—;
		arr[j] = arr[i]
	}

	arr[i] = temp;
	quickSort(arr,begin,i-1);
	
        quickSort(arr,i_1,end);
}
复制代码

冒泡排序

冒泡排序算法的运作如下:(从后往前) 1.比较相邻的元素。如果第一个比第二个大,就交换他们两个。 2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 3.针对所有的元素重复以上的步骤,除了最后一个。 4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

void popSort(int *arr, int len) {
    for (int i = 0; i < len; i++) {
        for (int j = 0; j < len - 1 - i; j++) {
            if (arr[j] < arr[j+1]) {
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}
复制代码

斐波拉契数列
int fib(int n) {
    if (n < 1) {
        return n;
    }
    int first = 0 ;
    int second = 1;
    for (int i = 0; i < n ; i++) {
        int sum = first + second;
        first = second;
        second = sum;
    }
    return second;
}

复制代码

链表是否有环

有环的定义是,链表的尾节点指向了链接中间的某个节点

快慢节点:创建指针1和指针2同时指向链表的头节点。进入循环,每次指针1向下移动一个节点,指针2向下移动2个节点。然后比较两个节点是否相等,如果等于则有环,如果不等于则进行下一次循环。

struct ListNode {
	int var;
	struct ListNode *next;
}

bool exitLoop (struct ListNode *head) {
	struct ListNode *fast ,*slow;
	fast = slow = head;
	while(slow!= NULL && fast->next != NULL ) {
		slow = slow->next;
		fast = fast->next->next;
		if(slow == fast) {
			return true;
		}
	}
	return false;
}
复制代码

字符串反转

思路:新建俩个指针begin,end, begin指向字符串头部,end指向尾部,循环交互数据,begin做加1操作,end做减1操作,直到begin>end

void char_reverse(char * cha) {
	char *begin = cha;
	char *end = cha + strlen(cha)-1;
	while(begin < end) {
		char temp = *begin;
		*(begin++) = *end;
		*(end—) = temp;
	}
}
复制代码

字符串回文
//void checkStr(char *cha) {
//    long x;
//    int i;
//    x = strlen(cha);
//    for(i = 0;i<=x/2;i++)///比到一半就不比了,原理已讲
//    {
//      if(cha[i]!=cha[x-i-1])///这就是比较两端的字符
//        {
//          break;//不是回文
//        }
//    }
//        if(i>x/2)///没执行break,就是回文
//            printf("YES\n");
//        else
//            printf("NO\n");
//}

复制代码

合并两个有序数组

思路:定义一个新数组,定义三个指针分别指向三个数组的第一个元素,比较两个数组的大小,将小的元素放入新数组,当一个数组放入结束后就将另一个全部放入新数组中。

void mergeList(int a[], int aLen, int b[], int bLen, int result[])
{
    int p = 0; // 遍历数组a的指针
    int q = 0; // 遍历数组b的指针
    int i = 0; // 记录当前存储位置
    
    // 任一数组没有到达边界则进行遍历
    while (p < aLen && q < bLen) {
        // 如果a数组对应位置的值小于b数组对应位置的值
        if (a[p] <= b[q]) {
            // 存储a数组的值
            result[i] = a[p];
            // 移动a数组的遍历指针
            p++;
        }
        else{
            // 存储b数组的值
            result[i] = b[q];
            // 移动b数组的遍历指针
            q++;
        }
        // 指向合并结果的下一个存储位置
        i++;
    }
    
    // 如果a数组有剩余
    while (p < aLen) {
        // 将a数组剩余部分拼接到合并结果的后面
        result[i] = a[p++];
        i++;
    }
    
    // 如果b数组有剩余
    while (q < bLen) {
        // 将b数组剩余部分拼接到合并结果的后面
        result[i] = b[q++];
        i++;
    }
}
复制代码

二分法查找
int binarySearch(int nums[], int target,int len) {
    int left = 0;
    int right =  len - 1; // 注意

    while(left <= right) { // 注意
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid;
        else if (nums[mid] < target)
            left = mid + 1; // 注意
        else if (nums[mid] > target)
            right = mid - 1; // 注意
        }
    return -1;
}
复制代码

查找两个父视图的共同子视图

思路:通过一个方法找出两个视图的所有父视图。用倒序方式获取各个视图的父视图。比较如果相等 则为共同父视图。如果不相等则退出循环。

- (NSArray <UIView *> *)findCommonSuperView:(UIView *)viewOne other:(UIView *)viewOther
{
    NSMutableArray *result = [NSMutableArray array];
    
    // 查找第一个视图的所有父视图
    NSArray *arrayOne = [self findSuperViews:viewOne];
    // 查找第二个视图的所有父视图
    NSArray *arrayOther = [self findSuperViews:viewOther];
    
    int i = 0;
    // 越界限制条件
    while (i < MIN((int)arrayOne.count, (int)arrayOther.count)) {
        // 倒序方式获取各个视图的父视图
        UIView *superOne = [arrayOne objectAtIndex:arrayOne.count - i - 1];
        UIView *superOther = [arrayOther objectAtIndex:arrayOther.count - i - 1];
        
        // 比较如果相等 则为共同父视图
        if (superOne == superOther) {
            [result addObject:superOne];
            i++;
        }
        // 如果不相等,则结束遍历
        else{
            break;
        }
    }
    
    return result;
}

- (NSArray <UIView *> *)findSuperViews:(UIView *)view
{
    // 初始化为第一父视图
    UIView *temp = view.superview;
    // 保存结果的数组
    NSMutableArray *result = [NSMutableArray array];
    while (temp) {
        [result addObject:temp];
        // 顺着superview指针一直向上查找
        temp = temp.superview;
    }
    return result;
}
复制代码

找出一万个里面前七位

合理的利用快速排序。

我们知道快排的思路是取出一个标识,来左右进行比较。

进行一轮比较之后我们得到的数组将以这个标识为中心,左边的全都小于它,右边的全都大于它。

利用这个思想,我们只需要判断这个标识所在的位置是否等于M即可。

当这个标识等于M时,输出前M个数即可,因为下标从零开始,所以当下标等于M时,输出0至M-1元素。


三十六个跑道找出跑得最快的3匹马

三十六个跑道找出跑得最快的3匹马


11、设计模式

MVVM

MVVM是Model-View-ViewModel的简写。 个人对MVVM的认知主要有两点。 1、解决了VC臃肿的问题,将逻辑代码、网络请求等都写入了VM中 2、数据的双向绑定。 iOS MVVM+RAC 从框架到实战


单例

有些面试官会让手写单例[Doge],也会追问不用dispatch_once,只用 !=nil行不行。 单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。 在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次),一般用于工具类。例如:登录控制器,网络数据请求,音乐播放器等一个工程需要使用多次的控制器或方法。注意:我们在使用单例类之前,一定要考虑好单例类是否适合和类以后的扩展性,避免盲目滥用单例。 ARC中单例实现步骤

  • 在类的内部提供一个static修饰的全局变量
  • 提供一个类方法,方便外界访问
  • 重写+allocWithZone方法,保证永远都只为单例对象分配一次内存空间
  • 严谨起见,重写-copyWithZone方法和-MutableCopyWithZone方法
  static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (_instance == nil) {
            _instance = [super allocWithZone:zone];
        }
    });
    return _instance;
}
// 为了使实例易于外界访问 我们一般提供一个类方法
// 类方法命名规范 share类名|default类名|类名
+(instancetype)shareTools
{
    //return _instance;
    // 最好用self 用Tools他的子类调用时会出现错误
    return [[self alloc]init];
}
// 为了严谨,也要重写copyWithZone 和 mutableCopyWithZone
-(id)copyWithZone:(NSZone *)zone
{
    return _instance;
}
-(id)mutableCopyWithZone:(NSZone *)zone
{
    return _instance;
}
复制代码

代理和通知的区别?

代理使用代理模式实现,而通知是使用观察者模式来实现的用于跨层传递消息的机制。 代理传递方式是一对一,而通知则是一对多


如何实现一个通知

(一个相对合理滴答案)在通知中心NSCenterNotification中可能会维护一个字典表。其key传递的要监听的名称,其value则是一个数组列表,转载着添加的观察者。


工厂模式

工厂模式可以简单概括为同类型不同型号的产品有各自对应的工厂进行生产。

ProductA、ProductB和ProductC继承自Product虚拟类,Show方法是不同产品的自描述; Factory依赖于ProductA、ProductB和ProductC,Factory根据不同的条件创建不同的Product对象。

iOS设计模式 - (3)简单工厂模式

iOS 的三种工厂模式


12、http部分

https与http的不同

HTTPS和HTTP的区别主要如下:

  1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

  2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

  3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

  4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。


https的传输过程

此段摘自HTTPS的工作原理 1.浏览器将自己支持的一套加密规则发送给网站。 2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。 3.获得网站证书之后浏览器要做以下工作: a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。 b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。 c) 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。 4.网站接收浏览器发来的数据之后要做以下的操作: a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。 b) 使用密码加密一段握手消息,发送给浏览器。 5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。 这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。


三次握手,如果两次可否
  • 首先Client向Server发送连接:SYN = 1, seq=x;

  • Server收到请求后 再向Client发送确认:SYN=1, ACK=1, seq=y, ack=x+1;

  • Client收到确认后还需再次发送确认,同时携带要发送给Server的数据:ACK=1, seq=x+1, ack= y+1;连接建立

为什么不能改成两次握手? 这主要是为了防止已失效的请求连接报文忽然又传送到了,产生错误。 假定A向B发送一个连接请求,由于一些原因,导致A发出的连接请求在一个网络节点逗留了比较多的时间。此时A会将此连接请求作为无效处理 又重新向B发起了一次新的连接请求,B正常收到此连接请求后建立了连接,数据传输完成后释放了连接。如果此时A发出的第一次请求又到达了B,B会以为A又发起了一次连接请求,如果是两次握手:此时连接就建立了,B会一直等待A发送数据,从而白白浪费B的资源。 如果是三次握手:由于A没有发起连接请求,也就不会理会B的连接响应,B没有收到A的确认连接,就会关闭掉本次连接。


四次挥手

第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。

第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。

第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。

第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。

挥手为什么需要四次?

因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。


浏览器访问服务器的流程
  • 浏览器输上域名
  • 若本地host文件无ip地址,浏览器会访问DNS服务器,把域名解析成ip地址,在返回给浏览器
  • 浏览器从ip中解析出端口号(应用层)
  • 浏览器通过解析后得到的ip和端口号来与web服务器建立一条TCP通道连接。也就是三次握手。并且把http的包分成帧(传输层)
  • 网络层把ip地址解析成服务器的mac地址
  • 包传输到数据链路层
  • 此后服务器按照相反的顺序把TCP分段的数据还原为http报文
  • 从sevlet中读取资料返回给浏览器

tcp和udp的区别
  • TCP面向连接的,可靠的数据传输服务;UDP面向无连接的,尽最大努力的数据传输服务,不保证数据传输的可靠性
  • TCP面向字节流,UDP面向报文
  • TCP有拥塞控制,UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有效,如直播,实时视频会议等)
  • TCP 只能是一对一的通信(TCP连接的端点是套接字socket),而 UDP 支持一对一、一对多、多对一和多对多的通信
  • TCP 的首部开销大,有 20 个字节,比 UDP 的 8 个字节的首部要长

get/put/post/delete请求方式

HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查,改,增,删4个操作。从语义上来说,GET请求是幂等的。


13、RN问题

RN能够运行的底层的原理

在 React Native 框架中,JSX 源码通过 React Native 框架编译后,通过对应平台的 Bridge 实现了与原生框架的通信。如果我们在程序中调用了 React Native 提供的 API,那么 React Native 框架就通过 Bridge 调用原生框架中的方法。 因为 React Native 的底层为 React 框架,所以如果是 UI 层的变更,那么就映射为虚拟 DOM 后进行 diff 算法,diff 算法计算出变动后的 JSON 映射文件,最终由 Native 层将此 JSON 文件映射渲染到原生 App 的页面元素上,最终实现了在项目中只需要控制 state 以及 props 的变更来引起 iOS 与 Android 平台的 UI 变更


RN的生命周期

1).创建阶段

该阶段主要发生在创建组件类的时候,在这个阶段中会初始化组件的属性类型和默认属性。通常会将固定的内容放在这个过程中进行初始化和赋值。 2,实例化阶段

该阶段主要发生在实例化组件类的时候,也就是该组件类被调用的时候触发。这个阶段会触发一系列的流程,按执行顺序如下:

constructor:构造函数,这里主要对组件的一些状态进行初始化。 componentWillMount:准备加载组件,可以在这里做一些业务初始化操作,或者设置组件状态。 render:生成页面需要的 DOM 结构,并返回该结构。 componentDidMount:组件加载成功并被成功渲染出来后执行。一般会将网络请求等加载数据的操作放在这里进行,保证不会出现 UI 上的错误。 3),运行(更新)阶段

该阶段主要发生在用户操作之后或者父组件有更新的时候,此时会根据用户的操作行为进行相应的页面结构的调整。这个阶段也会触发一系列的流程,按执行顺序如下:

componentWillReceiveProps:当组件接收到新的 props 时,会触发该函数。在该函数中,通常可以调用 this.setState 方法来完成对 state 的修改。 shouldComponentUpdate:该方法用来拦截新的 props 或 state,然后根据事先设定好的判断逻辑,做出最后要不要更新组件的决定。 componentWillUpdate:当上面的方法拦截返回 true 的时候,就可以在该方法中做一些更新之前的操作。 render:根据一系列的 diff 算法,生成需要更新的虚拟 DOM 数据。(注意:在 render 中最好只做数据和模板的组合,不应进行 state 等逻辑的修改,这样组件结构会更加清晰) componentDidUpdate:该方法在组件的更新已经同步到 DOM 中去后触发,我们常在该方法中做 DOM 操作。 4),销毁阶段

该阶段主要在组件消亡的时候触发。

componentWillUnmount:当组件要被从界面上移除时就会调用。可以在这个函数中做一些相关的清理工作,例如取消计时器、网络请求等。

RN与原生如何通信的

之前写的两篇文 React Native与原生通信(iOS端) RN与原生通讯(安卓篇)

Redux模式
uni-app的底层原理
如何看待跨平台开发
primose
RN的网络请求
热更新也会问一问

14、三方框架

WKWebViewJavascriptBridge的原理 原理就是在原生和JS双方都保存一个互相调用的信息,每个调用之间都有一个id和callbackid来找到两个环境对应的处理。

WebViewJavascriptBridge_JS.js用于对js环境bridge的处理,负责接收原生发送给js的消息,并且把js的消息发送给原生。 WKWebViewJavascriptBridge.m负责OC环境消息的处理,并且把OC环境的消息发送给js环境。 WebViewJavascriptBridgeBase.m主要实现了OC环境的bridge初始化和处理

再来看看web环境的初始化,刚说到web环境的操作是在bridge_js这个文件中完成的,这个文件调用一个setWebviewJavascriptBRidg的函数,并且这个函数传入的callback也是一个函数。callback函数中有我们在javascript环境中注册的OC调用JS提供的方法。在这个方法内部有一个iframe,它可以理解为webview的窗口,当我们改变iframe的src,相当于浏览器实现了链接的跳转。而一旦有跳转就会调用webview的代理方法。 在oc代理方法中,如果是来自js的消息,会调用injectJavascriptFile方法,这个方法的作用就是把WebViewJavascriptBridge_JS.js中的方法注入到webview中并且执行,从而达到初始化javascript环境的brige的作用


AFNetworking

源码太枯燥了,我画了一些图,吼吼吼。

AFNetworking的整体框架是怎样的呢? AFNetworking3.X的构成很简单,主要有四部分,除此之外还有一些基于UIKit的Category。

  • Manager : 负责处理网络请求的两个Manager,主要实现都在AFURLSessionManager中。
  • Reachability : 网络状态监控。
  • Security : 处理网络安全和HTTPS相关的。
  • Serialization : 请求和返回数据的格式化器。

在AFN3.0中,网络请求的manager主要有AFHTTPSessionManager和AFURLSessionManager构成,二者为父子关系。父类负责处理基础的网络请求,安全和监听的任务并且接受NSURLRequest对象,而子类则负责处理和http协议和序列化的逻辑。


简述AFN进行网络请求的流程

1、调用内部的dataTaskWithHttpRequest方法,该方法会生成一个task,并且调用[task resume]方法开启会话任务

1.1 该方法内部会调用requestWithHttpMethod方法,通过setValue对请求头部封装并且将请求参数转化为字符串的形式进行序列化,最终生成NSMutableRequest对象

2、当生成了MutableRequest对象后,就会调用内部的dataTaskWithRequest方法 2.1、调用内部的dataTaskWithRequst方法 2.2、创建AFURLSessionManagerTaskDelegate代理封装响应 2.3、通过setDelegateForTask绑定代理


sdwebimage

15、其他内容

git
podfile.lock文件
16进制转颜色
cocopods私有化搭建
远程推送原理

1.由App向iOS设备发送一个注册通知,用户需要同意系统发送推送。 2.iOS向APNs远程推送服务器发送App的Bundle Id和设备的UDID。 3.APNs根据设备的UDID和App的Bundle Id生成deviceToken再发回给App。 4.App再将deviceToken发送给远程推送服务器(自己的服务器), 由服务器保存在数据库中。 5.当自己的服务器想发送推送时, 在远程推送服务器中输入要发送的消息并选择发给哪些用户的deviceToken,由远程推送服务器发送给APNs。 6.APNs根据deviceToken发送给对应的用户

静态库和动态库

静态库是.a文件,编译时直接拷贝,不会改变,编译前设置需要拷贝的头文件。 动态库是.dylib,编译时不拷贝,运行时动态加载。 静态库的制作:

file->new project -> 选择Cocoa TouCh Static Library

在这里面可以添加一些新的文件

如何打包成静态库 静态库其实就是二进制文件 command+b编译得到libXXX.a,show infind

如何暴露出头文件给使用者?

模拟器和真机的架构是不一样的 ,所以每次编译生成的libGTStatic.a也是不一样的。但是我们生成的.a文件需要支持两个平台运行。因此需要将这两个文件合并成一个静态库。而上线的时候不需要模拟器架构的包。因此在debug模式才要合并。 资源打包的方式,内置了framework文件

我们制作的动态库跟系统的不一样,只是为了extension共享

在embedded binaries中加入,拷贝到项目


你觉着项目中遇见的最难的问题是什么
universelink
做一个下雨的动画
直播的一点原理

直播可以分为采集、前处理、编码、传输、解码、渲染这几个环节

  • 采集:直播视频的来源。摄像头、屏幕

  • 前处理:美颜、模糊效果、水印。iOS端:GPUImage,提供了丰富的预处理效果,也可以利用该库自定义设计

  • 编码:不经编码的视频非常庞大。通过压缩音视频数据来减少数据体积,方便音视频数据的推流、拉流和存储。能提供存储传输效率。编码方式:硬编码(非CPU,如显卡GPU)、软编码(使用CPU进行编码、手机容易发烫)。在iOS端提供了videobox来硬编码。

  • 传输:从推流端到服务端。很多情况下都使用RTMP模式

  • 流分发:将推过来的音频流转码生成不同的格式支持不同协议如RTMP/HLS/FLV以适应不同平台。

  • 播放:拉流获取到资源之后,需要通过解码器解码,渲染,才能在页面上播放。


IM单聊怎么实现的
数据库FMDB
组件化的内容。MGJRouter
音视频框架怎么用的
iOS中的锁

循环引用的问题
  • 代理引起的循环引用

代理方声明为strong类型,而委托方使用weak

举个🌰:假设有一个控制器是使用类(委托方),一个处理下载任务的类是功能类(代理方),在下载期间,控制器在对象势必是希望能一直拥有下载类对象的。因此控制器指向下载类对象的指针是strong类型。当下载任务结束之后,下载类对象需要用一个delegate指针指向控制器对象将执行的结果进行回传。在delegate指针也使用strong的情况下,控制器对象一旦需要释放时,会由于别的对象持有它而释放不掉,造成循环引用。


  • block引用

1、如果block对当前对象有一个截获,会对对象有一个强引用,而当前对象对block又有一个强引用,就会产生自循环的引用。可以通过__weak来解除循环引用。

用__weak修饰之后的对象block不会再对其进行retain,只是持有了weak指针,但是在block执行的过程中,该对象随时又有可能被释放,将weak指针置为空,可能会产生一些意料之外的错误,所以要用__strong修饰一下对其进行retain。 2、如果定义一个__block,在ARC下会产生循环引用。 可以采用断环的方式解决,但如果该block一直得不到调用,此循环就会一直出现

解决方案

__block XXBlock* blockSelf = self;
_blk = ^int(int num){
    int result = num*blockSelf.var;
    blockSelf = nil;
    return result;
};
_blk(3)
复制代码

  • NSTimer循环引用

定时器加在 runloop 上才会起作用,到达时间点后就会执行 action 方法,并且可以肯定这是一个对象方法。 定时器运行在主线程的 runloop 上,然后又回调方法,这个方法属于你当前这个VC的对象方法。既然是对象方法且能被调用,那么肯定所属的对象一定的要持有,因此这个对象被持有了。

 // 1.Block弱引用破除循环引用(https://www.cnblogs.com/H7N9/p/6540578.html)
 weak_self
 [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
     [weak_self dosomething];
 }];

 // 2.NSProxy消息转发实现破除循环引用(https://www.jianshu.com/p/1ef002fcf314)
 // proxy子类weak的target,然后实现target的消息转发。
 // 初始化proxy子类
 self.proxy = [WBProxy alloc];
 // 作为当前控制器的代理
 self.proxy.obj = self;
 // target为代理
 self.timer = [NSTimer timerWithTimeInterval:1 target:self.proxy selector:@selector(timerEvent) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

复制代码
  • 非OC对象内存处理

注意以creat,copy作为关键字的函数都是需要释放内存的,注意配对使用。比如:CGColorCreate<-->CGColorRelease

CIImage *beginImage = [[CIImage alloc]initWithImage:[UIImage imageNamed:@"yourname.jpg"]];
...
CGImageRelease(ref);//非OC对象需要手动内存释放
复制代码

三、真题

因为时间仓促,没有记完整。

小米

  • TCP和UDP,三次握手,追问改成两次会怎样,线程同步机制
  • 斐波拉契数列
  • 反转链表
  • RN跟原生相比的优劣,组件生命周期,项目基于RN哪个版本开发,双端适配问题
  • 平常如何学习,性能优化,项目难点在哪
  • 事件传递机制和响应机制
  • 什么时候view不会响应触摸事件,如何响应view以外的事件
  • 自动释放池
  • 内存管理机制
  • weak关键词在什么情况下用,与assgin的区别
  • weak底层原理
  • 什么时候会报unrecognized selector异常,底层实现原理,消息发送
  • runloop
  • 使用过哪些三方库
  • 问了些组件化的问题

美团外卖商家端

  • uni-app这个东西是啥,你觉着咋样
  • 从前台到后台的一次请求经历了哪些阶段?
  • http/https的工作过程和区别、它们属于五层中的哪一层
  • 三次握手的原理
  • get/put/post/delete请求方式
  • 项目里面你做了什么性能优化和启动改造
  • 为什么WKWebview的性能比UIWebview的性能高
  • runtime的消息转发机制
  • runtime的具体用法
  • KVC
  • @synthesize和@dynamic分别有什么作用
  • 36匹马,六个跑道。没有记时器等设备,用最少的比赛次数算出跑的最快的马
  • setNeedsDisplay、setNeedsLayout、layoutIfNeed区别
  • MGJRouter的作用

货拉拉

  • 项目里面值得认可的部分、RN桥接的一些东西
  • MVVM和MVC
  • 调试奔溃的工具
  • 做了什么操作让优化项目
  • get和post、http协议、网络请求这块问得比较多
  • 数据传输的格式json?xml?等等
  • 后台返回的数据是多少K的,大文件传输怎么办
  • 数据库的一些操作
  • iOS存储方式
  • TCP/IP协议
  • ARC引用计数、内存管理几个区域
  • OC的基础数据类型
  • 说一说多线程、在项目中怎么用的
  • 单例模式怎样设计的,有什么优缺点
  • 观察者模式、KVO的原理
  • 地图开发经验
  • 写uni-app的架构、serveice层、小程序上线流程
  • 为什么要写博客
  • 以后的发展方向

滴滴

  • 二叉树搜索的思路
  • 反转链表
  • 项目中遇到的重大问题、如何解决的、解决后心态的变化
  • 组件化有用到哪些?说说你们后台分发的流程
  • block的循环引用是怎么产生的?在内部会不会加上__strong?为什么
  • block如何修改外部变量
  • runtime消息转发流程和结构体
  • GCD和NSOperation的利弊
  • 说一下runloop
  • 图片缓存的原理和图片移除的原理(主要考察最长时间算法)
  • 你知道的数据持久化的方式
  • 数据库用的哪些?FMDB、SQLite
  • 在读写数据库的时候使用的是几个线程
  • 说一下uni-app、这样设计架构的目的是什么
  • 单例、代理、观察者你喜欢用哪个
  • swift的struct和class的区别
  • swift的option是怎么实现的
  • 做了哪些性能优化和启动改造
  • 怎么与前端进行通信的

快手用户增长

  • 聊了一下过往的工作

  • weak的用途和原理。追问:key和value是怎样对应的?查找时怎样找到它清理的对象。

  • uitableview是什么模式实现的?

  • frame和bounds的区别

  • kvo的使用和原理

  • isa指针的说明

  • base64/md5加密/aes/rsa+des加密,在什么情况下用到了md5加密

md5是一个非对称的校验算法。验证数据的真实性

  • block的结构和它造成的循环引用

  • 自动化打包

平台通过脚本控制 远程打包 节省人力物力,解决冲突,走测试

打包时间长怎么优化:bitcode是否开启 编译选项是否选上,模拟器环境去掉 编译过程缩短:可执行文件 .o .h ipa包打开会看见,链接网络环境。

  • Autolayout,举了个栗子排布。uibutton宽度固定,uilabel怎么排

  • 离屏渲染的原理和例子

  • http传输的五层,一个浏览器向服务端发送url的过程。追问:解析ip地址的时候发生在哪一层。

  • 三次握手的原理,tcp在哪一层?ip在哪一层

  • 快速排序的复杂度、冒泡排序的复杂度

  • 手写两个链表合并

  • 手写字符串反转


趣拿

  • HTTPS和HTTP的区别
  • HTTPS传值过程
  • runtime发送消息的流程。给nil对象发送消息会怎样?
  • 根元类的isa指向,根元类的父类指向
  • 用runtime做过什么事情
  • Hook和aop
  • load和initail的调用时机
  • rn模块开发,反向传值是怎么传的
  • 同时重写getter和setter方法会怎样?
  • KVO的原理。追问:如果对这个类使用了KVC,再用反射方法去访问这个类,它返回的结果是什么?
  • 静态库动态库

……后面聊太多不记得了……

  • 手写链表反转
  • 怎么判断有环
  • 二分查找
  • 斐波拉契
  • 快排

猿题库

  • oc的动态运行时体现在什么方面
  • runtime发送消息的流程,forwardTarget方法是做什么的?
  • kvo的实现过程
  • 要是让你设计一个通知模式,你会怎么做?怎么把方法传给外面要调用它的类?
  • crash的捕获原理
  • 两个透明的view都有button,view1放在view2上面,但是从界面看上去button像是放在一个view中。问此时点击两个button会发生什么事。
  • NSTimer什么时候会出现循环引用?怎么解决
  • RN的原理
  • HTTPS和HTTP的区别、五层模型
  • fps的检测
  • cs的基础
  • shell和ruby有写过脚本么
  • 找出数组中比它左边都要大比它右边要小的数放入到新的数组
  • wkwebview比uiwebview的区别

emm……这一年确实发生过很多事,当时多多少少也有些狼狈吧(well,可以说很狼狈了(,,´•ω•)ノ"(´っω•`。)),有人被莫名推着往前走,连说放弃的权力都没有,也有人倾其全部,却换来个一无所有。寓言故事《落难的王子》有一段描述,大意是:任何一件事落在谁的头上他都得受着,并且他一定承受得起。对于目前的生活状态呢,其实还是挺感激的,所以接下来就是好好沉淀自己,提升一下技术,跟小伙伴们相处时能更友善一些。目标没有明朗之前,不想再去瞎折腾了。 愿珍惜当下所拥有的,也愿每个人都能被世界温柔以待。

文章分类
iOS
文章标签