前段时间使用公司封装的空白页占位视图工具,工具是对DZNEmptyDataSet框架的封装。这个框架以前在许多项目也都用过,却没有认真阅读过源码,真的很遗憾。这两天趁五一放假有空,将DZNEmptyDataSet框架学习了一遍,感觉收获满满。
其中重要感悟如下:
1.代码使用简单:主要逻辑在UIScrollView+EmptyDataSet分类中完成。使用时只需要设置控制器为其数据源和代理,并实现相应的代理方法。
2.对runtime合理使用:利用runtime的关联功能实现分类中属性的getter、setter;利用runtime的method的IMP指针重置功能进行reloadData等方法交换。
3.提出了以前使用runtime方法交换的隐藏缺陷,并给出解决方案。
4.修改对空白列表占位视图的响应链传递路径。
5.采用NSLayoutConstraint+VFL(Visual Format Language)“可视化格式语言”进行设置约束,重温Apple原生方法的魅力。
 
使用入口
1.导入UIScrollView分类UIScrollView+EmptyDataSet
#import <DZNEmptyDataSet/UIScrollView+EmptyDataSet.h>
2.设置tableView的数据源对象和代理对象
self.tableView.emptyDataSetSource = self;
self.tableView.emptyDataSetDelegate = self;
 
核心思想和重要方法
核心思想
1.在客户端调用属性设置时进行方法交换,监听reloadData方法
self.tableView.emptyDataSetSource = self;
在设置方法setEmptyDataSetSource 内部,通过runtime进行reloadData的方法交换。
通过监听reloadData的数据源个数,来决定是否显示空白页占位视图。
 
2.runtime中提出传统IMP Swizzle的缺陷和隐藏问题,并给出了新的解决方案。
OC方法的底层实现是C语言的运行时函数,而Runtime函数默认的前两个参数是id, SEL。
OBJC_EXPORT id _Nullable
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

平时用的runtime函数交换方法会改变原始函数的方法名,其对应的C函数就是参数SEL。

void method_exchangeImplementations(方法m1,方法m2)
如果原始函数在底层根据SEL做了逻辑操作,那么无意间就会修改了系统底层的原始逻辑,这是很危险的!
 
DZNEmptyDataSet中给出的解决方案是:
在代码中定义C函数并将其强转(IMP)dzn_original_implementation。
交互原来的实现IMP为新的C函数 method_setImplementation(method, (IMP)dzn_original_implementation)。
存储原来旧的实现IMP到全局搜索表 _impLookupTable。
全局搜索表 _impLookupTable在整个生命周期内记录UITableView,UICollectionView,UIScrollView,目的是只为交互一次。
 
重要方法
1.数据源setter方法
- (void)setEmptyDataSetSource:(id<DZNEmptyDataSetSource>)datasource
{
if (!datasource || ![self dzn_canDisplay]) {
[self dzn_invalidate];
} objc_setAssociatedObject(self, kEmptyDataSetSource, [[DZNWeakObjectContainer alloc] initWithWeakObject:datasource], OBJC_ASSOCIATION_RETAIN_NONATOMIC); // We add method sizzling for injecting -dzn_reloadData implementation to the native -reloadData implementation
[self swizzleIfPossible:@selector(reloadData)]; // Exclusively for UITableView, we also inject -dzn_reloadData to -endUpdates
if ([self isKindOfClass:[UITableView class]]) {
[self swizzleIfPossible:@selector(endUpdates)];
}
}
DZNWeakObjectContainer:用来包裹外部传递过来的数据源对象
swizzleIfPossible:对reloadData方法进行runtime交换
 
2.reload交换方法:
static NSMutableDictionary *_impLookupTable;
static NSString *const DZNSwizzleInfoPointerKey = @"pointer";
static NSString *const DZNSwizzleInfoOwnerKey = @"owner";
static NSString *const DZNSwizzleInfoSelectorKey = @"selector"; - (void)swizzleIfPossible:(SEL)selector
{
// Check if the target responds to selector
if (![self respondsToSelector:selector]) {
return;
} // Create the lookup table
if (!_impLookupTable) {
_impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:]; // 3 represent the supported base classes
} // We make sure that setImplementation is called once per class kind, UITableView or UICollectionView.
for (NSDictionary *info in [_impLookupTable allValues]) {
Class class = [info objectForKey:DZNSwizzleInfoOwnerKey];
NSString *selectorName = [info objectForKey:DZNSwizzleInfoSelectorKey]; if ([selectorName isEqualToString:NSStringFromSelector(selector)]) {
if ([self isKindOfClass:class]) {
return;
}
}
}
//1.根据target 返回对应的类class
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
//2.根据class名和selector,创建一个dzn_implement组合key
NSString *key = dzn_implementationKey(baseClass, selector);
//3.根据class名和selector组合key,拿到交换的implement指针。
NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey]; // If the implementation for this class already exist, skip!!
if (impValue || !key || !baseClass) {
return;
} // Swizzle by injecting additional implementation
Method method = class_getInstanceMethod(baseClass, selector);
//4.将C函数dzn_original_implementation设置成Selector的新的IMP,并返回旧的IMP指针。
IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation); // Store the new implementation in the lookup table(源码注解错误,应该是old implementation,可以点击函数method_setImplementation查看验证)
// 存储旧的reload涵数指针IMP到全局查询表_impLookupTable (正确注释)
NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass,
DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]}; [_impLookupTable setObject:swizzledInfo forKey:key];
}
_impLookupTable保存在app的数据存储区,整个app周期只保存一份数据,所以可以保证整个app生命周期UITableView, UICollectionView, UIScrollView只能交换一次。
在C函数dzn_original_implementation中注入自定义操作,并将函数指针强转成IMP,绑定给原始Method上。
将旧的,原始的函数指针IMP(如:reloadData)存贮到全局查询列表_impLookupTable中,对应的key为:DZNSwizzleInfoPointerKey。
 
3.自定义注入C函数:
void dzn_original_implementation(id self, SEL _cmd)
{
// Fetch original implementation from lookup table
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
NSString *key = dzn_implementationKey(baseClass, _cmd); NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key];
NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey]; IMP impPointer = [impValue pointerValue]; // We then inject the additional implementation for reloading the empty dataset
// Doing it before calling the original implementation does update the 'isEmptyDataSetVisible' flag on time.
[self dzn_reloadEmptyDataSet]; // If found, call original implementation
if (impPointer) {
((void(*)(id,SEL))impPointer)(self,_cmd);
}
}
将self和_cmd组合成key, 从全局查询表_impLookupTable拿到原始IMP函数指针
然后,执行自定义方法[self dzn_reloadEmptyDataSet]
然后,执行原始IMP函数
 
4.空白视图添加方法
- (void)dzn_reloadEmptyDataSet
//空白视图添加方法
if (!view.superview) {
// Send the view all the way to the back, in case a header and/or footer is present, as well as for sectionHeaders or any other content
if (([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]]) && self.subviews.count > ) {
[self insertSubview:view atIndex:];
}
else {
[self addSubview:view];
}
} //更新内部子视图约束
[view setupConstraints];
对于UITableView,UICollectionView,存在子视图的容器View,将占位视图添加到层级为0的位置。
对于一般的单纯View,则直接添加。
 
5.更新内部子视图约束
- (void)setupConstraints
{
// First, configure the content view constaints
// The content view must alway be centered to its superview
NSLayoutConstraint *centerXConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterX];
NSLayoutConstraint *centerYConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterY]; [self addConstraint:centerXConstraint];
[self addConstraint:centerYConstraint];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options: metrics:nil views:@{@"contentView": self.contentView}]]; // When a custom offset is available, we adjust the vertical constraints' constants
if (self.verticalOffset != && self.constraints.count > ) {
centerYConstraint.constant = self.verticalOffset;
}
DZNEmptyDataSet采用的是NSLayoutConstraint+VFL(Visual Format Language),“可视化格式语言”。
我们平时用的比较多是Monsary,对于苹果原生的使用反而不多,在学习此框架的同时,可以趁机回顾一下原生的魅力。
 
6.修改响应链
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event]; // Return any UIControl instance such as buttons, segmented controls, switches, etc.
if ([hitView isKindOfClass:[UIControl class]]) {
return hitView;
} // Return either the contentView or customView
if ([hitView isEqual:_contentView] || [hitView isEqual:_customView]) {
return hitView;
} return nil;
}
对于点击事件的处理,DZNEmptyDataSetView采用的是定向响应传递。
如果点击的范围在_contentView,_customView,UIControl类型,就直接返回,不在继续向下寻找。
 
重要角色
1.工具类
UIView (DZNConstraintBasedLayoutExtensions),作用:
快速为当前视图的子视图生成一个约束。
DZNWeakObjectContainer : NSObject,作用:
Weak对象容器
 
2.空白页展示视图View
DZNEmptyDataSetView : UIView,作用:
创建空白页展示视图的UI控件,添加手势事件,控件的垂直偏移和距离。
更新子视图约束
修改响应链
 
3.核心逻辑类
UIScrollView (DZNEmptyDataSet),作用:
UIScrollView分类属性(DataSource, Delegate, emptyDataSetView)保存,利用runtime的objc_getAssociatedObject进行getter, setter 。
监听reloadData方法,endUpdates方法并进行方法交换,利用runtime方法method_setImplementation(method, (IMP)dzn_original_implementation);
另:在分类下添加扩展UIScrollView () <UIGestureRecognizerDelegate>,增加了私有属性emptyDataSetView。

静态类结构

DZNEmptyDataSet框架阅读的更多相关文章

  1. DZNEmptyDataSet框架简介

    给大家推荐一个设置页面加载失败时显示加载失败等的框架. 下载地址:DZNEmptyDataSet https://github.com/dzenbot/DZNEmptyDataSet 上效果首先在你的 ...

  2. 软件体系架构之ssh框架阅读笔记

    首先我们要了解一下什么是ssh框架? SSH是 struts+spring+hibernate的一个集成框架,是目前比较流行的一种Web应用程序开源框架. ssh框架系统从职责上分为四层:web层 业 ...

  3. DRF框架和Vue框架阅读目录

    Vue框架目录 (一)Vue框架(一)——Vue导读.Vue实例(挂载点el.数据data.过滤器filters).Vue指令(文本指令v-text.事件指令v-on.属性指令v-bind.表单指令v ...

  4. ios开发——实用技术篇OC篇&iOS的主要框架

    iOS的主要框架         阅读目录 Foundation框架为所有的应用程序提供基本系统服务 UIKit框架提供创建基于触摸用户界面的类 Core Data框架管着理应用程序数据模型 Core ...

  5. 深入理解jQuery框架-框架结构

    这是本人结合资料视频总结出来的jQuery大体框架结构,如果大家都熟悉了之后,相信你们也会写出看似高档的js框架: jquery框架的总体结构 (function(w, undefined){ //定 ...

  6. jQuery源代码框架思路

    開始计划时间读源代码,第一节jQuery框架阅读思路整理 (function(){ jQuery = function(){}; jQuery一些变量和函数和给jQuery对象加入一些方法和属性 ex ...

  7. Spring源码分析专题 —— 阅读指引

    阅读源码的意义 更深入理解框架原理,印象更深刻 学习优秀的编程风格.编程技巧.设计思想 解决实际问题,如修复框架中的bug,或是参考框架源码,结合实际业务需求编写一个独有的框架 阅读源码的方法 首先是 ...

  8. 一步步去阅读koa源码,整体架构分析

    阅读好的框架的源码有很多好处,从大神的视角去理解整个框架的设计思想.大到架构设计,小到可取的命名风格,还有设计模式.实现某类功能使用到的数据结构和算法等等. 使用koa 其实某个框架阅读源码的时候,首 ...

  9. 结合个人经历总结的前端入门方法 (转自https://github.com/qiu-deqing/FE-learning)

    结合个人经历总结的前端入门方法 (https://github.com/qiu-deqing/FE-learning),里面有很详细的介绍. 之前一直想学习前端的,都不知道怎么下手都一年了啥也没学到, ...

随机推荐

  1. LightOJ 1123 Trail Maintenance

    题意:n个城市m天.每一天修一条道路,输出当前天数的最小生成树,但是这里有一个条件,就是说最小生成树必须包括全部n个城市,否则输出-1 思路:边数有6000如果每一天跑一次最小生成树的话就接近O(m^ ...

  2. [转]C#操作Memcached帮助类

    在VS中安装Memcached,直接在NuGet下搜索Memcached,选择第一个进行安装: 服务端资源下载地址:https://pan.baidu.com/s/1gf3tupl 接下来开始写程序, ...

  3. [转]【转】大型高性能ASP.NET系统架构设计

    大型高性能ASP.NET系统架构设计 大型动态应用系统平台主要是针对于大流量.高并发网站建立的底层系统架构.大型网站的运行需要一个可靠.安全.可扩展.易维护的应用系统平台做为支撑,以保证网站应用的平稳 ...

  4. Hex编码

    编码原理 Hex编码就是把一个8位的字节数据用两个十六进制数展示出来,编码时,将8位二进制码重新分组成两个4位的字节,其中一个字节的低4位是原字节的高四位,另一个字节的低4位是原数据的低4位,高4位都 ...

  5. 51nod 1282 时钟

    1282 时钟  题目来源: Codility 基准时间限制:1 秒 空间限制:131072 KB 分值: 40 难度:4级算法题 有N个时钟,每个时钟有M个指针,P个刻度.时钟是圆形的,P个刻度均分 ...

  6. vue-learning:32 - component - 异步组件和工厂函数

    异步组件 只有在这个组件需要使用的时候才从服务器加载这一个组件模块,用于渲染,并且会把结果缓存起来供未来复用. 实现方法: 组件定义的时候,以一个工厂函数的形式传入,在需要组件的执行这个函数,然后将组 ...

  7. 一个vue管理系统的初步搭建总结

    ps:目前这个项目只是有一个大致的框架,并没有做完 前期准备工作 前端构建工具:Visual Studio Code后端构建工具:IDEA数据库和服务器构建工具:WampServer (使用的是2.4 ...

  8. Little Elephant and Array CodeForces - 220B (莫队)

    The Little Elephant loves playing with arrays. He has array a, consisting of npositive integers, ind ...

  9. C#面试题整理(不带答案)

     1.维护数据库的完整性.一致性.你喜欢用触发器还是自写业务逻辑?为什么?  2.什么是事务?什么是锁?  3.什么是索引,有什么优点?  4.视图是什么?游标是什么?  5.什么是存储过程?有什么优 ...

  10. Percona-XtraDB-Cluster 5.7版本(PXC)集群部署

    PXC(Percona-XtraDB-Cluster)5.7版本集群部署 Centos 7.3系统部署Mysql 集群 PXC三个节点分别执行和安装(1)配置hosts cat /etc/hosts1 ...