TOC
首先是对帮助过我的大哥大姐们来一波感谢?在我换工作这段时间里有内推的,答疑解惑的,送吃的甚至还有看风水的(/ω\),多少让Lisa这个小透明有些受宠若惊了。真的非常感谢大家对我的帮助和建议。
也许是年关将至,身边很多朋友听到我要换工作的消息,大多态度都是酱紫的:
“傻狍子,你知道今年多难么”
“至少撑到年后啊”
对于这些想法我只能……举双手双jio赞同。但是能怎么办呢,我把公司干倒了┓( ´∀· )┏,不动一动恐怕连馒头皮都吃不起。干巴爹!
这个月也面了大大小小二三十家公司了,就我的观察来说,中型公司岗位确实很少,大公司和外派岗位相对多些,而小公司就算面上了也压工资并且……有些还歧视女生。一上来就问生没生娃啊,能不能接受007。之前也跟几个朋友沟通过,现在也算是入职高峰期,年底要求并不是特别高,很多岗位在一月份就开始停止招聘了。因此个人感觉虽然今年行情不是特别好,但机会还是很多的。
这次也是准备得稍微仓促了些,毕竟平常开发以业务为主。除了对自己项目进行总结之外,底层方面的我看了下面的资料:
<( ̄▽ ̄)/讲真,我没看完。
此处绝对没有营销嫌疑,穷得只能啃馒头皮的我常年混迹于N手市场,平常也喜欢搜集一些好的资料。就单纯觉着这些资料还行。
emmm……简历被内推的大佬打回来N次,也参照了很多模板。跟改毕业论文似得。简单来说就不要整那些花里胡哨的排版吧,然后star法则把自己的经历说清楚就好。当然,要对简历上所写的每个字都负责。不然被问到达不出来会有点尴尬啊 ̄□ ̄||
能整理出来的尽量整理,有些博客比我写的好,就直接粘了
这真的是……必问题。大概14/15的概率吧。
先粘一篇很nice的博客镇楼。
OC的动态特性表现为了三个方面:动态类型、动态绑定、动态加载;
一个OC对象包含着id和class,id是objc_object
类型的结构体,里面是一个isa指针,指向它的类。而class是objc_class
的结构体,继承自objc_object
,还包含着super class,用来缓存指针和虚函数表的cache
和bits
(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、消息的动态解析
会去判断是否已经解析过,若没有解析过,才会去根据这个方法是类方法还是实例方法去调用resolveClassMethod
和resolveInstanceMethod
方法。而在这两个方法中开发者就可以写一些比如说class_addMethod
的代码去动态添加一些方法。如果程序找到了动态方法,就会讲动态解析标记成yes,再去动态调用这个方法。若没找到动态解析,则会进入到消息转发的过程。
3、消息的转发
一进入会调用forwardingTargetForSelector
的方法,如果该方法实现则直接调用,若没实现会调用msgSignatureForSelector
做一个方法签名,然后再调用forgetInvocation
这样一个方法。如果说这两个方法开发者都没实现,程序将会调用doesNotRecoginize
然后报没有找到该方法的错误。
检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。
检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉。
@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];
}
}
关联对象由AssociationsManager
管理并在AssociationsHashMap
中管理。所有对象的关联内容都在同一全局容器中。
实现:通过关联策略(比如copy、retain等)将它的key(关联值)和(关联值的value)封装成objectAssociation
的结构,然后把这个结构和key封装成objectAssociationMap
,最后将该map与key结合放入全局的AssociationsHashMap
中
当setObject传成nil的时候,就能清除关联对象。
当policy为OBJC_ASSOCIATION_ASSIGN的时候,设置的关联值将是以weak的方式进行内存管理的。
#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
根元类的isa指向它自己,父类是指向NSObject
说白了,AOP编程就是在运行时动态的将代码切入到类的指定方法、指定位置上的编程方式。
把消息的实现转给别的接收者去处理。比如说一个person类,调用了run方法,但是它没有实现。最后会走到forgetTargetForSelector。此时car类有run方法,我们直接返回car类,这样就会调用car类的方法。
forwardingTargetForSelector仅支持一个对象的返回,也就是说消息只能被转发给一个对象
forwardInvocation可以将消息同时转发给任意多个对象
一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其实内部就是do-while循环,这个循环内部不断地处理各种任务(比 如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。
在 CoreFoundation 里面关于 RunLoop 有5个类:CFRunLoopRef
、CFRunLoopModeRef
、CFRunLoopSourceRef
、CFRunLoopTimerRef
、CFRunLoopObserverRef
其中CFRunLoopModeRef
类并没有对外暴露,只是通过 CFRunLoopRef
的接口进行了封装。他们的关系如下:
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
[nsthread alloc]initwithblock
方法中加入一个runloop
保持子线程的生命,在别的地方通过performforselector
去做线程想要做的事情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,并且当前时间与休眠之前时间差值大于阈值,就表示卡顿了。
load是在main函数之前装载文件时被调用的,只被调用一次,而且它不是采用objc_msgSend方式调用,而是直接采用函数的内存地址调用。多个类的load跟complile source中的文件有关,根据文件从上而下调用,子类和父类同时实现load方法时,父类优先子类。分类和本类都有load方法,优先本类
initialize:当类或子类第一次收到消息时被调用只调用一次
调用方式是通过runtime的objc_msgSend的方式调用的,此时所有的类都已经装载完毕
子类和父类同时实现initialize,父类的先被调用,然后调用子类的
本类与category同时实现initialize,category会覆盖本类的方法,只调用category的
NSSet是无序集合,在内存中是不连续的,集合中的元素不可重复。内部有hash算法,因此查找效率更高。应用场景有配合NSArry做去重处理,cell的重用机制是随机在重用池中获取一个cell
NSArray是有序集合,在内存中连续,应用场景是tableview的数据源的数据要按照其下标取值。
系统会报这个错误
Use of undeclared identifier '_name';did you mean 'name'
解决方法是
@synthesize wtName = _wtName;
原因:用@property声明的成员属性,相当于自动生成了setter getter方法,如果重写了set和get方法,与@property声明的成员属性就不是一个成员属性了,是另外一个实例变量,而这个实例变量需要手动声明。所以会报错误。@synthesize 声明的属性=变量。意思是,将属性的setter,getter方法,作用于这个变量
readOnly
readWrite
atomic
(默认):保证对成员属性的赋值和获取是线程安全的(不包括操作。例如对一个被atomic修饰的数组进行删除或添加是不在atomic作用范围内的)
noatomic
retain
**/**strong
:retain通常在mrc中使用。这两个关键字都是用来修饰对象的
assgin
**/**unsafe_unretain
: assgin既可以用于修饰基础数据类型也可以修饰对象。在修饰对象类型时是不会改变引用计数的。assgin指针仍然指向原地址,此时若assgin指针仍去访问原地址,则可能由于悬垂指针而导致内存泄露。unsafe_unretain只有在mrc中使用比较频繁
copy
weak
atomic表示的是原子性。内部是用自旋锁实现。声明属性时,编译器会自动生成getter/setter
方法,最终调用objc_getProperty
和objc_setProperty
方法来进行属性的存取。此时使用atomic,这两个方法内部会用os_unfair_lock
进行加锁,来保证读写的原子性。锁都在propertylocks
里保存着,在用之前会把锁初始化好,要用的时候通过对象地址加上成员变量的偏移量为key,去propertylocks
取。存取时用的是同一个锁,所以atomic能保证属性的存取时是线程安全的。
atomic为什么不能保证绝对的线程安全?
atomic在getter/setter方法中加锁,仅保证了存取时的线程安全,假设我们的属性是@property(atomic)NSMutableArray *array;
可变的容器时,无法保证对容器的修改是线程安全的
在编译器自动生产的getter/setter方法,最终会调用objc_getProperty
和objc_setProperty
方法存取属性,在此方法内部保证了读写时的线程安全的,当我们重写getter/setter
方法时,就只能依靠自己在getter/setter
中保证线程安全
对象在运行时获取其类型的能力称为内省。内省可以有多种方法实现。
-(BOOL) isKindOfClass:
判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass:
判断是否是这个类的实例
-(BOOL) respondsToSelector:
判断实例是否有这样方法
+(BOOL) instancesRespondToSelector:
判断类是否有这个方法
反射就是利用字符串去动态的检测,从而实现运行时的转化。
weak用于修饰对象,而assgin既可以修饰基本数据类型也可以修饰对象
assgin修饰的对象被释放之后,assgin指针仍指向原对象的地址,而weak在被修饰的对象释放之后会自动置为nil
浅拷贝:让目标对象指针和源对象指针都指向同一内存空间。所以它会增加被拷贝对象的引用计数。
深拷贝:让目标对象指针和源对象指针分别指向内容相同的两块内存空间。因此不会增加被拷贝对象的引用计数。
由上图可以看出,对于可变对象的copy和mutableCopy都是深拷贝。不可变对象的copy是浅拷贝,mutableCopy是深拷贝。copy方法返回的都是不可变对象。
copy关键字返回的目标对象都是不可变的。而源对象被声明为可变数组类型,就会有调用方对其进行添加或移除数据的操作。而此时被拷贝的结果是一个不可变对象,就容易引起某些程序异常的问题
原理:CPU计算好屏幕要显示的内容(如视图创建,布局计算,图片解码,文本绘制等)放入缓存中,GPU从缓存中读取数据等待显示器的垂直同步信号(V-Sync)发出进行渲染,再将结果放入到帧缓冲区,随后视频控制器读取帧缓冲区的数据,将其转换并传递给显示器显示。
注:iOS是双缓冲区
当用户触摸屏幕时,iOS系统会将事件加入到UIApplication
管理的一个任务队列中,UIApplication
会将处于任务最前端的事件分发到UIWindow
中,继而分发到UIView
中。UIView
会查看自身是否能处理事件,触摸点是否自己身上。若能则以相同的方式遍历子控件。若在子控件中没有找到,则自身就是事件处理者。若自身不能处理,则不做任何操作。
UIView不能接受事件处理的情况有如下三种:
// 此方法返回的View是本次点击事件需要的最佳View
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
// 判断一个点是否落在范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
事件传递给控件后,就调用hitTest
方法寻找更合适的控件,如果子控件是合适,则在子控件中再调用上述方法,直到找到最合适的控件或废弃事件。当父控件不可用时,返回nil,子控件也将无法继续寻找合适的view。
创建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);
}
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取值的实现:按照getKey、key、isKey的顺序搜索这三个方法,若某一方法被实现,取到的即是方法返回的值。后面的方法不再运行。若没有找到则会调用accessInstanceVariablesDirectly方法判断是否允许取成员变量的值。如果返回NO,会直接调用valueForUndefinedKey方法、若返回yes,会按先后顺序取_key、_isKey、key、isKey的值。若都没取到,还是会调用valueForUndefineKey方法
KVC设置的实现:首先搜索是否有setKey方法,如果没有会调用accessInstanceVariablesDirectly方法判断是否能直接访问成员变量。若不能则调用setValueForUndefinedKey方法,抛出异常。若返回yes,则按照_key、_isKey、key、isKey的顺序搜索成员名。如果还未搜到同样会调用setValueForUndefinedKey方法。
在-(id)valueForKey:(NSString *)key
和-(void)setValue:(id)value forKey:(NSString *)key
方法中的key都是没有限制的。也就是说,只要外界知道key的名字,就可以获取或者访问它。因此从这个角度来说,kvc是有违背面向对象的思想的。
假设有一个类A。当外界调用addObeserver
方法时,系统会在动态运行时创建NSKVONotifying_A
的类继承于原来的类,同时将类A的isa指针指向新创建的类。在新创建的这个类中,重写Setter
方法,以达到通知所有观察对象的目的。
-(void)setValue:(id)obj {
[self willChangeValueForKey:@"keyPath"];
[super setValue:obj];
[self didChangeValueForKey:@"keyPath"];
}
答:在对成员变量赋值的时候,可以在赋值前调用willChangeValueForKey
方法,而在赋值之后调用didChangeValueForKey
方法。didChangeValueForKey
在系统内部实现当中会触发KVO回调。
UIView可以看做是CALayer的管理者,它为CALayer提供内容,还可以处理触摸等事件,参与响应链。而CALayer继承自NSObject,无法响应事件,只负责显示内容。
UIView中的layer属性可以返回它的主图层的实例,该属性的内部,维护着三份拷贝,即逻辑树,动画树(进行各种渲染操作)和显示树(显示屏幕上的内容)。
UIView拥有的layerClass方法,可以返回主图层所使用的类。UIView的子类,通过重载该方法,可以实现使用不同的CALayer来显示内容。
为啥UIView负责提供内容,而CALayer负责显示?
答:单一职责。
这块要看个人项目了,我们还有一个操作就是第一次打开webview的时候,把token存入了cookie中,前端同学也会做一系列操作。比如说当token失效的时候,会主动与原生的桥进行通信,原生端收到消息后跳转到登录页面。推荐一个还不错的处理cookie的三方库叫做GGCookie
分为卡顿优化、电池、内存优化、安装包瘦身、冷启动优化。
在规定的16.7ms之内,下一帧VSync信号到来之前,并没有CPU和GPU共同完成一帧画面的合成,则这一帧就会被丢弃掉。可以通过core Animation
查看FPS值。
Debug->Color Blended Layers
查看混合图层。红色为混合图层。
比如说UIlabel显示中文时,由于实际渲染的区域大于label的size,最外层多了一个sublayer,如果不设置label.layer.masksToBounds = YES
,那第二行的label边缘就会出现红色。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 处理;网络请求;定位;图像渲染;
冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。
第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。
dyld(Dynamic Link Editor)
,Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)。启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。
启动 App 时,调用 map_images 进行可执行文件的内容解析和处理,再 load_images 中调用 call_load_methods 调用所有 Class 和 Category 的 load 方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor))
修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。
在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions:
函数。
针对不同的阶段,有不同的优化思路:
减少动态库、合并动态库,定期清理不必要的动态库;
减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;
减少 C++ 虚函数数量;
Swift 开发尽量使用 struct;
用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))
、C++ 静态构造器、以及 Objective-C 中的 load 方法;
将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中;
调用了未实现的实例方法,有三次转发机会可以处理,如果这三次都没处理,才会崩溃在doesNotRecognizeSelector:中,所以我们可以利用这个原理来避免崩溃。这三次转发:resolveInstanceMethod:、forwardingTargetForSelector:和methodSignatureForSelector:+forwardInvocation:,其中最适合用来处理的可能就是第三次转发了,因为我们会用分类的方式重写方法,尽量在消息转发越往后的阶段影响越小。因为使用分类重写,要注意工程是否有其他类也重写了这两个方法。
@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:。
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];
}
有个简单的方法:给NSObject 增加一个分类,然后利用Run time 交换系统的 removeObserver方法,在里面添加 @try @catch。
// 交换后的方法
- (void)removeDasen:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
@try {
[self removeDasen:observer forKeyPath:keyPath];
} @catch (NSException *exception) {}
}
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修饰的变量变成了对象。通过变量的forwarding指针去找到对应的对象,对其进行赋值。
{
NSMutableArray *array = nil;
void(^Block)(void) = ^{
array = [NSMutableArray array];
};
Bkock();
}
要在array的声明处添加__block修饰符
全局:NSConcreateGlobalBlock
栈:NSConcreateStackBlock
堆:NSConcreateMallocBlock
如果对__block变量不进行拷贝,操作的就是栈上的__block变量,如果发生了拷贝,无论操作栈还是堆上的__block变量,都是使用了堆上的__block变量。
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];
});
}
有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需要配合NSOperationQueue实现多线程编程
-(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];
weak是Runtime维护了一个hash(哈希)表,用于存储指向某个对象的所有weak指针。在添加弱引用变量时,最终会调用weak_register_no_lock(),它根据hash算法进行查找,若查找的位置已经有了当前对象对应的弱引用数组,就把新的变量添加到该数组中,若无对应的弱引用数组,则重新创建弱引用数组,在第0个位置加上该变量,其他位置初始化为0。
在对象调用dealloc的时候,内部会最终会调用weak_clear_no_lock方法,它会更加当前指针查找弱引用表,把当前对象对应的弱引用都拿出来得到一个数组,遍历该数组的所有弱引用指针,分别置为nil.
SideTable包含了自旋锁、弱引用表和引用计数表。自旋锁是一种忙等锁,适用于轻量访问
如果不是通过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是由LLVM(编译器)和runtime协作的结果。ARC中禁止手动调用retain/realse/retainCount/dealloc(可重写delloc但不能显式调用)
方法。当我们编译源码的时候,编译器会分析源码中每个对象的生命周期,然后基于这些对象的生命周期,来添加相应的引用计数操作代码。ARC中新增了weak/strong关键字属性关键字。
源码下载:runtime开放源码
在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
是装所有分类的数组,在这段方法中,采用<u>倒序</u>遍历。mlist
是二维数组,装载分类的方法,属性和协议列表。rw->methods.attachLists(mlists,mcount)
表示将二维方法列表添加到原来宿主类的列表中。属性列表与协议列表的添加原理亦是如此。
这段引自:iOS底层原理 - category的本质以及源码分析
通过以上分析可以得出Category
的加载处理过程:
首先由runtime加载某个类的所有分类数据,将分类中的方法、属性、协议数据都合并到一个大数组中。而由于是倒序的方式遍历,所以后面参与编译的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方法。
作用
特点
分类是运行时决议,而扩展是编译时决议(因此扩展可以直接添加成员变量,而分类要通过关联对象的形式)
分类可声明可实现,而扩展只有声明。它的实现要在宿主类中。
可以为系统类添加分类,但不能为系统类添加扩展。
get
方法和set
方法,而非实例变量。(当然类别也可以通过关联对象来添加实例变量)。其实宿主类的方法还是存在的。只是在做内存拷贝的过程中,分类的方法会位于方法列表靠前的位置,由于消息动态转发机制获取到相匹配的方法名就不会再往下寻找。因此,在代码执行的时候只会调用分类的方法。
<u>问:是否能给分类添加”成员变量“?如果能,那么这个变量被放到了哪里?如何做销毁操作呢?</u>
- (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传给AssociationsHashMap
,AssociationsHashMap
存储着所有的关联对象。
只挑出了常问的,链表反转几乎是必问的了。
头插法思路:定义一个新的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的数字。将arrj的数字填入到arri中。然后开始移动i指针,当i指向小于基准数的时候略过,i继续向右移动。当它有值大于temp的时候,将arri填入arrj中,继续轮换移动两个指针。直到i和j相遇,此时将temp的填入到arri中。
//要是从左往右开始找,当i停在了比基准值大的位置上,与j相遇,将这个值与基准值交换的话,就不符合条件了,
//也可能会出现i=j,且ai与aj的值都比基准值大
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元素。
MVVM是Model-View-ViewModel的简写。
个人对MVVM的认知主要有两点。
1、解决了VC臃肿的问题,将逻辑代码、网络请求等都写入了VM中
2、数据的双向绑定。
有些面试官会让手写单例Doge,也会追问不用dispatch_once,只用 !=nil行不行。
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。
在整个应用程序中,共享一份资源(这份资源只需要创建初始化1次),一般用于工具类。例如:登录控制器,网络数据请求,音乐播放器等一个工程需要使用多次的控制器或方法。注意:我们在使用单例类之前,一定要考虑好单例类是否适合和类以后的扩展性,避免盲目滥用单例。
ARC中单例实现步骤
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对象。
HTTPS和HTTP的区别主要如下:
1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
此段摘自HTTPS的工作原理
1.浏览器将自己支持的一套加密规则发送给网站。
2.网站从中选出一组加密算法与HASH算法,并将自己的身份信息以证书的形式发回给浏览器。证书里面包含了网站地址,加密公钥,以及证书的颁发机构等信息。
3.获得网站证书之后浏览器要做以下工作:
a) 验证证书的合法性(颁发证书的机构是否合法,证书中包含的网站地址是否与正在访问的地址一致等),如果证书受信任,则浏览器栏里面会显示一个小锁头,否则会给出证书不受信的提示。
b) 如果证书受信任,或者是用户接受了不受信的证书,浏览器会生成一串随机数的密码,并用证书中提供的公钥加密。
c) 使用约定好的HASH计算握手消息,并使用生成的随机数对消息进行加密,最后将之前生成的所有信息发送给网站。
4.网站接收浏览器发来的数据之后要做以下的操作:
a) 使用自己的私钥将信息解密取出密码,使用密码解密浏览器发来的握手消息,并验证HASH是否与浏览器发来的一致。
b) 使用密码加密一段握手消息,发送给浏览器。
5.浏览器解密并计算握手消息的HASH,如果与服务端发来的HASH一致,此时握手过程结束,之后所有的通信数据将由之前浏览器生成的随机密码并利用对称加密算法进行加密。
这里浏览器与网站互相发送加密的握手消息并验证,目的是为了保证双方都获得了一致的密码,并且可以正常的加密解密数据,为后续真正数据的传输做一次测试。
为什么不能改成两次握手?
这主要是为了防止已失效的请求连接报文忽然又传送到了,产生错误。
假定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报文,因此不能一起发送。故需要四次挥手。
HTTP中的GET,POST,PUT,DELETE就对应着对这个资源的查,改,增,删4个操作。从语义上来说,GET请求是幂等的。
在 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 变更
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:当组件要被从界面上移除时就会调用。可以在这个函数中做一些相关的清理工作,例如取消计时器、网络请求等。
之前写的两篇文
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的整体框架是怎样的呢?
AFNetworking3.X的构成很简单,主要有四部分,除此之外还有一些基于UIKit的Category。
在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绑定代理
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中加入,拷贝到项目
直播可以分为采集、前处理、编码、传输、解码、渲染这几个环节
strong
类型,而委托方使用weak
。举个?:假设有一个控制器是使用类(委托方),一个处理下载任务的类是功能类(代理方),在下载期间,控制器在对象势必是希望能一直拥有下载类对象的。因此控制器指向下载类对象的指针是strong类型。当下载任务结束之后,下载类对象需要用一个delegate指针指向控制器对象将执行的结果进行回传。在delegate指针也使用strong的情况下,控制器对象一旦需要释放时,会由于别的对象持有它而释放不掉,造成循环引用。
用__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)
// 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];
CIImage *beginImage = [[CIImage alloc]initWithImage:[UIImage imageNamed:@"yourname.jpg"]];
...
CGImageRelease(ref);//非OC对象需要手动内存释放
因为时间仓促,没有记完整。
打包时间长怎么优化:bitcode是否开启 编译选项是否选上,模拟器环境去掉
编译过程缩短:可执行文件 .o .h ipa包打开会看见,链接网络环境。
emm……这一年确实发生过很多事,当时多多少少也有些狼狈吧(well,可以说很狼狈了(,,´•ω•)ノ"(´っω•`。)),有人被莫名推着往前走,连说放弃的权力都没有,也有人倾其全部,却换来个一无所有。寓言故事《落难的王子》有一段描述,大意是:任何一件事落在谁的头上他都得受着,并且他一定承受得起。对于目前的生活状态呢,其实还是挺感激的,所以接下来就是好好沉淀自己,提升一下技术,跟小伙伴们相处时能更友善一些。目标没有明朗之前,不想再去瞎折腾了。
愿珍惜当下所拥有的,也愿每个人都能被世界温柔以待。