目录
- 对象引用计数放哪里?
- MVVM和MVC的区别
- UIButton防止多次点击
- 如何监听弱网
- 卡顿检测
- NSCache,NSDictionary,NSArray的区别
- SDWebImage里面用了哪种缓存策略?
- self + weakSelf + strongSelf ?
一 对象引用计数放哪里?
我们先看看 struct objc_class
的结构
再看看 isa
结构
- 在arm64架构之前,isa就是一个普通的
指针
,存储着Class、Meta-Class对象的内存地址
- 从arm64架构开始,对isa进行了优化,变成了一个
共用体(union)
结构,还使用位域
来存储更多的信息
- 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
中
相关参考
iOS-底层原理(13)-runtime之isa详解
iOS-底层原理(14)isa-Class的结构详解
二 MVVM和MVC的区别
2.1 MVC
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混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。
2.2 MVVM
一种可以很好地解决Massive View Controller
问题的办法就是将 Controller 中的展示逻辑抽取出来,放置到一个专门的地方,而这个地方就是 viewModel
。MVVM衍生于MVC,是对 MVC 的一种演进,它促进了 UI 代码与业务逻辑的分离。它正式规范了视图和控制器紧耦合的性质,并引入新的组件。他们之间的结构关系如下:
2.2.1 MVVM 的基本概念
- 在
MVVM
中,view
和view controller
正式联系在一起,我们把它们视为一个组件 -
view
和view controller
都不能直接引用model
,而是引用视图模型(viewModel
) -
viewModel
是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方 - 使用
MVVM
会轻微的增加代码量,但总体上减少了代码的复杂性
2.2.2 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
的覆辙,变得难以维护。
2.2.3 MVVM 的优势
- 低耦合:
View
可以独立于Model
变化和修改,一个viewModel
可以绑定到不同的View
上 - 可重用性:可以把一些视图逻辑放在一个
viewModel
里面,让很多view
重用这段视图逻辑 - 独立开发:开发人员可以专注于业务逻辑和数据的开发
viewModel
,设计人员可以专注于页面设计 - 可测试:通常界面是比较难于测试的,而
MVVM
模式可以针对viewModel
来进行测试
2.2.4 MVVM 的弊端
- 数据绑定使得
Bug
很难被调试。你看到界面异常了,有可能是你View
的代码有Bug
,也可能是Model
的代码有问题。数据绑定使得一个位置的Bug
被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。 - 对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
- 数组内容的转化成本较高:数组里面每项都要转化成
Item
对象,如果Item对象中还有类似数组,就很头疼。 - 转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
- 只有在API返回的数据高度标准化时,这些对象原型(
Item
)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。 - 调试时通过对象原型查看数据内容不如直接通过
NSDictionary/NSArray
直观。 - 同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。
2.3 总结
MVC
的设计模式也并非是病入膏肓,无药可救的架构,最起码目前MVC设计模式仍旧是iOS开发的主流框架,存在即合理。针对文章所述的弊端,我们依旧有许多可行的方法去避免和解决,从而打造一个轻量级的ViewController
。MVVM
是MVC
的升级版,完全兼容当前的MVC架构,MVVM虽然促进了UI 代码与业务逻辑的分离,一定程度上减轻了ViewController
的臃肿度,但是View
和ViewModel
之间的数据绑定使得 MVVM变得复杂和难用了,如果我们不能更好的驾驭两者之间的数据绑定,同样会造成Controller 代码过于复杂,代码逻辑不易维护的问题。一个轻量级的
ViewController
是基于MVC
和MVVM
模式进行代码职责的分离而打造的。MVC和MVVM有优点也有缺点,但缺点在他们所带来的好处面前时不值一提的。他们的低耦合性,封装性,可测试性,可维护性和多人协作便利大大提高了开法效率。同时,我们需要保持的是一个拥抱变化的心,以及理性分析的态度。在新技术的面前,不盲从,也不守旧,一切的决策都应该建立在认真分析的基础上,这样才能应对技术的变化。
相关参考
iOS 关于MVC和MVVM设计模式的那些事
三 UIButton防止多次点击
3.1 设置enabled或userInteractionEnabled属性
通过UIButton
的enabled
属性和userInteractionEnabled
属性控制按钮是否可点击。此方案在逻辑上比较清晰、易懂,但具体代码书写分散,常常涉及多个地方。
- (void)tapBtn:(UIButton *)btn {
NSLog(@"按钮点击...");
btn.enabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
btn.enabled = YES;
});
}
3.2 借助cancelPreviousPerformRequestsWithTarget:selector:object实现
通过 NSObject 的两个方法
实现步骤如下:
- 1 创建一个
UIButton
的分类,使用runtime
增加public
属性cs_eventInterval
和private
属性cs_eventInvalid
。 - 2 在
+load
方法中使用runtime
将UIButton
的-sendAction:to:forEvent:
方法与自定义的cs_sendAction:to:forEvent:
方法进行交换 - 3 使用
cs_eventInterval
作为控制cs_eventInvalid
的计时因子,用cs_eventInvalid
控制UIButton
的event
事件是否有效。
// 此方法会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
// 多长时间后做某件事情
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
/** 方法一 */
- (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(@"真正开始执行业务 - 比如网络请求...");
}
3.3 通过runtime交换方法实现
@interface UIButton (Extension)
/** 时间间隔 */
@property(nonatomic, assign)NSTimeInterval cs_eventInterval;
@end
#import "UIButton+Extension.h"
#import
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);
}
#pragma mark - click
- (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];
}
}
#pragma mark - set | get
- (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);
}
推荐参考 iOS-UIButton防止重复点击(三种办法)
四 如何监听弱网
查阅了一些资料,暂时还没有编码实现,后期会更新
参考文章
iOS下的实际网络连接状态检测
[前端测试] 弱网测试方法整理
五 卡顿检测
参考以下文章
质量监控-卡顿检测
简单监测iOS卡顿的demo
六 NSCache,NSDictionary,NSArray的区别
6.1 NSArray
NSArray作为一个存储对象的有序集合,可能是被使用最多的集合类。
性能特征
在数组的开头和结尾插入/删除元素通常是一个O(1)操作,而随机的插入/删除通常是 O(N)的。
有用的方法
NSArray的大多数方法使用isEqual:来检查对象间的关系(例如containsObject:)。有一个特别的方法
indexOfObjectIdenticalTo:
用来检查指针相等,如果你确保在同一个集合中搜索,那么这个方法可以很大的提升搜索速度。
更多相关资料参考
6.2 NSDictionary
一个字典存储任意的对象键值对。 由于历史原因,初始化方法使用相反的对象到值的方法,
[NSDictionary dictionaryWithObjectsAndKeys:object, key, nil]
而新的快捷语法则从key开始
@{key : value, ...}
NSDictionary中的键是被拷贝的并且需要是恒定的。如果在一个键在被用于在字典中放入一个值后被改变,那么这个值可能就会变得无法获取了。一个有趣的细节,在NSDictionary中键是被拷贝的,而在使用一个toll-free桥接的CFDictionary时却只被retain。CoreFoundation类没有通用对象的拷贝方法,因此这时拷贝是不可能的(*)。这只适用于使用CFDictionarySetValue()的时候。如果通过setObject:forKey使用toll-free桥接的CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成立 — 转换为CFDictionary的NSDictionary对象,对其使用CFDictionarySetValue()方法会调用回setObject:forKey并拷贝键。
6.3 NSCache
NSCache是一个非常奇怪的集合。在iOS 4/Snow Leopard中加入,默认为可变
并且线程安全
的。这使它很适合缓存那些创建起来代价高昂的对象。它自动对内存警告
做出反应并基于可设置的成本
清理自己。与NSDictionary相比,键是被retain而不是被拷贝
的。
NSCache的回收方法是不确定的,在文档中也没有说明。向里面放一些类似图片那样比被回收更快填满内存的大对象不是个好主意。(这是在PSPDFKit中很多跟内存有关的crash的原因,在使用自定义的基于LRU的链表的缓存代码之前,我们起初使用NSCache存储事先渲染的图片。)
NSCache可以设置撑自动回收实现了NSDiscardableContent协议的对象。实现该属性的一个比较流行的类是同时间加入的NSPurgeableData,但是在OS X 10.9之前,是非线程安全的(没有信息表明这是否也影响到iOS或者是否在iOS 7中被修复了)。
NSCache性能
那么NSCache如何承受NSMutableDictionary的考验?加入的线程安全必然会带来一些消耗。
6.4 iOS 构建缓存时选 NSCache 而非NSDictionary
当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减 时间最久为被使用的对象
NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便
NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了
相关资料参考
iOS7: 漫谈基础集合类(NSArray,NSSet,NSOrderedSet,NSDictionary,NSMapTable,NSHashTable, NSPointerArray, NSIndexSet,NSCache, NSFastEnumeration)
七 SDWebImage里面用了哪种缓存策略?
使用了NSCache
做缓存策略,具体的参考下面的文章,写的挺详细的
推荐参考文章
iOS缓存 NSCache详解及SDWebImage缓存策略源码分析
八 self + weakSelf + strongSelf ?
__weak __typeof(self)weakSelf = self; //1
[self.context performBlock:^{
[weakSelf doSomething]; //2
__strong __typeof(weakSelf)strongSelf = weakSelf; //3
[strongSelf doAnotherSomething];
}];
- 解释说明
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的生命周期.
总结
- 在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。
- 如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。
推荐参考文章
iOS-Block 中 为何使用 weakSelf 和 strongSelf