作者:路飞_Luck&&jianshu.com/p/f9ec208b00b6
- 对象引用计数放哪里?
- MVVM和MVC的区别
- UIButton防止多次点击
- 弱网相关
- 卡顿检测
- NSCache,NSDictionary,NSArray的区别
- SDWebImage里面用了哪种缓存策略?
- self + weakSelf + strongSelf ?
参考内容
-
可查阅objc相关源码:struct objc_class -> isa
-
isa 结构如下
/** isa_t 结构体 */ union isa_t { Class cls; uintptr_t bits; struct { uintptr_t nonpointer : 1; uintptr_t has_assoc : 1; uintptr_t has_cxx_dtor : 1; uintptr_t shiftcls : 33; uintptr_t magic : 6; uintptr_t weakly_referenced : 1; uintptr_t deallocating : 1; uintptr_t has_sidetable_rc : 1; uintptr_t extra_rc : 19; }; };
-
extra_rc:表示该对象的引用计数值,实际上是引用计数值减 1,例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10,则需要使用到下面的 has_sidetable_rc。
-
has_sidetable_rc:当对象引用计数大于 10 时,则has_sidetable_rc 的值为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中,这是一个散列表。
-
对象引用计数就存放在extra_rc中
参考内容较长,可直接看总结
参考内容
-
MVC:V(<Update&&Action>)C (Update> &&<Notify) M
-
MVC的弊端
-
厚重的View Controller
- M:模型model的对象通常非常的简单。根据Apple的文档,model应包括数据和操作数据的业务逻辑。而在实践中,model层往往非常薄,不管怎样,model层的业务逻辑不应被拖入到controller。
- V:视图view通常是UIKit控件(component,这里根据习惯译为控件)或者编码定义的UIKit控件的集合。View的如何构建(PS:IB或者手写界面)何必让Controller知晓,同时View不应该直接引用model(PS:现实中,你懂的!),并且仅仅通过IBAction事件引用controller。业务逻辑很明显不归入view,视图本身没有任何业务。
- C:控制器controller。Controller是app的“胶水代码”:协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的loading、appearing、disappearing等等,同时往往也会充满我们不愿暴露的model的模型逻辑以及不愿暴露给视图的业务逻辑。网络数据的请求及后续处理,本地数据库操作,以及一些带有工具性质辅助方法都加大了Massive View Controller的产生。
- 遗失(无处安放)的网络逻辑 苹果使用的MVC的定义是这么说的:所有的对象都可以被归类为一个model,一个view,或是一个controller。
-
你可能试着把它放在Model对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的model生命周期更长,事情将变的复杂。显然View里面做网络请求那就更格格不入了,因此只剩下Controller了。若这样,这又加剧了Massive View Controller的问题。若不这样,何处才是网络逻辑的家呢?
-
较差的可测试性 由于View Controller混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。
-
-
MVVM
一种可以很好地解决Massive View Controller问题的办法就是将 Controller 中的展示逻辑抽取出来,放置到一个专门的地方,而这个地方就是 viewModel 。MVVM衍生于MVC,是对 MVC 的一种演进,它促进了 UI 代码与业务逻辑的分离。它正式规范了视图和控制器紧耦合的性质,并引入新的组件。他们之间的结构关系如下:
-
MVVM 基本概念
- 在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件 view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel) viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方 使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
-
MVVM 的注意事项
- view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
- viewModel 引用model,但反过来不行* MVVM 的使用建议
- MVVM 可以兼容你当下使用的MVC架构。
- MVVM 增加你的应用的可测试性。
- MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。
- viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
- viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
- viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。
- viewModel之间可以有依赖。
- viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
-
MVVM 的优势
- 低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上
- 可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
- 独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
- 可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
-
MVVM 的弊端
- 数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
- 对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
- 数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
- 转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
- 只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
- 调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
- 同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。
-
总结
-
MVC的设计模式也并非是病入膏肓,无药可救的架构,最起码目前MVC设计模式仍旧是iOS开发的主流框架,存在即合理。针对文章所述的弊端,我们依旧有许多可行的方法去避免和解决,从而打造一个轻量级的ViewController。
-
MVVM是MVC的升级版,完全兼容当前的MVC架构,MVVM虽然促进了UI 代码与业务逻辑的分离,一定程度上减轻了ViewController的臃肿度,但是View和ViewModel之间的数据绑定使得 MVVM变得复杂和难用了,如果我们不能更好的驾驭两者之间的数据绑定,同样会造成Controller 代码过于复杂,代码逻辑不易维护的问题。
-
一个轻量级的ViewController是基于MVC和MVVM模式进行代码职责的分离而打造的。MVC和MVVM有优点也有缺点,但缺点在他们所带来的好处面前时不值一提的。他们的低耦合性,封装性,可测试性,可维护性和多人协作便利大大提高了开法效率。
-
同时,我们需要保持的是一个拥抱变化的心,以及理性分析的态度。在新技术的面前,不盲从,也不守旧,一切的决策都应该建立在认真分析的基础上,这样才能应对技术的变化。
-
参考内容
- userInteractionEnabled - 通过UIButton的enabled属性和userInteractionEnabled属性控制按钮是否可点击。 - 此方案在逻辑上比较清晰、易懂,**但具体代码书写分散**,常常涉及多个地方。-
cancelPreviousPerformRequestsWithTarget:selector:object
- 总结:会出现延时现象,并且需要对大量的UIButton做处理,工作量大,不方便。
/** 方法一 */ - (void)tapBtn:(UIButton *)btn { NSLog(@"按钮点击了..."); // 此方法会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(buttonClickedAction:) object:btn]; // 多长时间后做某件事情 [self performSelector:@selector(buttonClickedAction:) withObject:btn afterDelay:2.0]; } - (void)buttonClickedAction:(UIButton *)btn { NSLog(@"真正开始执行业务 - 比如网络请求..."); }
-
通过Runtime交换UIButton的响应事件方法,从而控制响应事件的时间间隔。
- 创建一个UIButton的分类,使用runtime增加public属性cs_eventInterval和private属性cs_eventInvalid。
- 在+load方法中使用runtime将UIButton的-sendAction:to:forEvent:方法与自定义的cs_sendAction:to:forEvent:方法进行交换
- 使用cs_eventInterval作为控制cs_eventInvalid的计时因子,用cs_eventInvalid控制UIButton的event事件是否有效。
@interface UIButton (Extension) /** 时间间隔 */ @property(nonatomic, assign)NSTimeInterval cs_eventInterval; @end static char *const kEventIntervalKey = "kEventIntervalKey"; // 时间间隔 static char *const kEventInvalidKey = "kEventInvalidKey"; // 是否失效 @interface UIButton() /** 是否失效 - 即不可以点击 */ @property(nonatomic, assign)BOOL cs_eventInvalid; @end @implementation UIButton (Extension) + (void)load { // 交换方法 Method clickMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:)); Method cs_clickMethod = class_getInstanceMethod(self, @selector(cs_sendAction:to:forEvent:)); method_exchangeImplementations(clickMethod, cs_clickMethod); } - (void)cs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { if (!self.cs_eventInvalid) { self.cs_eventInvalid = YES; [self cs_sendAction:action to:target forEvent:event]; [self performSelector:@selector(setCs_eventInvalid:) withObject:@(NO) afterDelay:self.cs_eventInterval]; } } - (NSTimeInterval)cs_eventInterval { return [objc_getAssociatedObject(self, kEventIntervalKey) doubleValue]; } - (void)setCs_eventInterval:(NSTimeInterval)cs_eventInterval { objc_setAssociatedObject(self, kEventIntervalKey, @(cs_eventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)cs_eventInvalid { return [objc_getAssociatedObject(self, kEventInvalidKey) boolValue]; } - (void)setCs_eventInvalid:(BOOL)cs_eventInvalid { objc_setAssociatedObject(self, kEventInvalidKey, @(cs_eventInvalid), OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
参考内容
- 合适的超时时间,针对不同网络设定不同的超时时间,加快超时,尽快重试 - 按子模块多请求去拉取数据,避免一次性加载,导致数据太多请求返回慢; - 缓存和增量请求 - 优化DNS查询:应尽量减少DNS查询,做DNS缓存,避免域名劫持、DNS污染,同时把用户调度到“最优接入点”。 - 减小数据包大小和优化包量:通过压缩、精简包头、消息合并等方式,来减小数据包大小和包量。 - 优化ACK包:平衡冗余包和ACK包个数,达到降低延时,提高吞吐量的目的。(这些难度有点高) - 断线重连,因为我们是 socket 通信的,所以需要做断线重连,重连时间可以递增 - 减少数据连接的创建次数,由于创建连接是一个非常昂贵的操作,所以应尽量减少数据连接的创建次数,且在 - 一次请求中应尽量以批量的方式执行任务。如果多次发送小数据包,应该尽量保证在2秒以内发送出去。在短时间内访问不同服务器时,尽可能地复用无线连接。 - 用户 UI 体验优化,加载一些动画什么的分散下注意力 - 根据不同网络情况动态调节方案,比如图片下载和视频流就可以按照 3G 4G 和 WIFI 进行区分返回 - HTTP3.0- 通过监测Runloop的kCFRunLoopAfterWaiting,用一个子线程去检查,一次循环是否时间太长。
- 如果超出预定时间,则可以dump堆栈,确定卡顿函数
参考内容
- NSArray - 在数组的开头和结尾插入/删除元素通常是一个O(1)操作,而随机的插入/删除通常是 O(N)的。 - NSDictionary - NSDictionary中的键是被拷贝的并且需要是恒定的。如果在一个键在被用于在字典中放入一个值后被改变,那么这个值可能就会变得无法获取了。一个有趣的细节,在NSDictionary中键是被拷贝的,而在使用一个toll-free桥接的CFDictionary时却只被retain。CoreFoundation类没有通用对象的拷贝方法,因此这时拷贝是不可能的(*)。这只适用于使用CFDictionarySetValue()的时候。如果通过setObject:forKey使用toll-free桥接的CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成立 — 转换为CFDictionary的NSDictionary对象,对其使用CFDictionarySetValue()方法会调用回setObject:forKey并拷贝键。 - NSCache - 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减 时间最久为被使用的对象 - NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便 - NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了- 使用了NSCache做缓存策略
参考内容
- 在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。 - 如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。1.使用__weak __typeof是在编译的时候,另外创建一个局部变量weak对象来操作self,引用计数不变。
block 会将这个局部变量捕获为自己的属性,
访问这个属性,从而达到访问 self 的效果,因为他们的内存地址都是一样的。
2.因为weakSelf和self是两个变量,doSomething有可能就直接对self自身引用计数减到0了.
所以在[weakSelf doSomething]的时候,你很难控制这里self是否就会被释放了.weakSelf只能看着.
3.__strong __typeof在编译的时候,实际是对weakSelf的强引用.
指针连带关系self的引用计数会增加.但是你这个是在block里面,生命周期也只在当前block的作用域.
所以,当这个block结束, strongSelf随之也就被释放了.不会影响block外部的self的生命周期.
__weak __typeof(self)weakSelf = self; //1
[self.context performBlock:^{
[weakSelf doSomething]; //2
__strong __typeof(weakSelf)strongSelf = weakSelf; //3
[strongSelf doAnotherSomething];
}];
- 面试题系列目录
- 上一份: 苏州蜗牛iOS开发面试题2018年春
- 下一份: 阿里字节一套高效的iOS面试题2020年2月