前段时间使用公司封装的空白页占位视图工具,是对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]; // 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]) {
Classclass = [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:[UITableViewclass]] || [self
isKindOfClass:[UICollectionViewclass]]) && self.subviews.count > 1) { [self
insertSubview:view atIndex:0]; } 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:0 metrics:nil views:@{@"contentView":
self.contentView}]];// When a custom offset is available, we adjust the
vertical constraints' constants if (self.verticalOffset != 0 &&
self.constraints.count >0) { 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:[UIControlclass]]) { 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。
静态类结构



 

 

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信