DZNEmptyDataSet是一个无数据时的空白页展示框架,支持自定义文字和图片,用起来很方便。最近研究了一下它的源代码,学习到了很多编程上的技巧。
基本用法以UITableView为例,UITableView只需要遵循DZNEmptyDataSet的代理方法,代理方法设置空白页显示文字和图片即可,具体这里不详细讲解:
_table.emptyDataSetSource = self;
_table.emptyDataSetDelegate = self;
此框架其实就是UIScrollView类的一个分类,因为UITableView,UICollectionView都继承自UIScrollView,设计思路是给UIScrollView动态添加代理属性,在setter方法中判断当前类是否可以对reloadData进行Swizzle,也就是自定义方法的注入。作者没有用 method_exchangedImplementations() 方法进行方法交换,因为目的就是指明了要对原生的reloadData方法进行修改,所以为了防止reloadData方法在其他第三方里被提前替换了,作者这里建了一张表,以key:value 的形式对原生方法进行储存, 用C函数定义IMP,假如其他第三方用method_exchangedImplementations()方法对reloadData做了运行时的替换,[self swizzle_reloadData]虽然调用的是原生方法,但是在IMP接收到的 _cmd参数不是@selector(reloadData)而是@selector(swizzle_reloadData)(这里可以查看我的上一篇文章),这时用查询表在IMP中进行检测判断_cmd是否是原生方法,是的话在调用原生方法之前插入自己的操作;不是就返回。
就从这两个代理入手,第一个emptyDataSetSource点进去:
是其中的一个属性,其set方法为:
objc_setAssociatedObject动态添加set方法,关键部分在于[self swizzleIfPossible:@selector(reloadData)];
方法的字面意思是:是否可以进行swizzle;因为UIScrollView不响应reloadData方法。上面注释说:我们添加swizzing方法是为了在原生的reloadData方法的实现里注入-dzn_reloadData的实现。也就是在原生方法 reloadData 之前添加自定义的方法实现。接下来看具体操作:
- (void)swizzleIfPossible:(SEL)selector
{
// Check if the target responds to selector
//检查当前类是否实现了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.
//确保每个类中当前的selector的实现方法被调用了一次
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;
}
}
}
//如果查询表单中啥都没有,那么拿到当前的类,一个以当前类名和方法名复合成的字符串key,反向查找对应的method中的IMP值是否存在
//获取当前类
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
//这里是封装了一个方法,拼接baseClass和selector成为一个字符串,如:UITableView_reloadData
NSString *key = dzn_implementationKey(baseClass, selector);
//查看一下这个key有没有在列表里存在过
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);
//用新的IMP代替,method_setImplementation()返回的还是原生方法只不过IMP改变了实现方式
IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation);
// Store the new implementation in the lookup table
//把新的实现方法存入查询列表中
NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass,
DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]};
[_impLookupTable setObject:swizzledInfo forKey:key];
}
dzn_original_implementation是一个C语言方法:
//此方法是获取method中的IMP指针,并在指针指向原生方法之前注入自定义方法
void dzn_original_implementation(id self, SEL _cmd)
{
// Fetch original implementation from lookup table
// _cmd代表当前调用方法,注意:如果项目中其他地方有用method_exchangedImplementations()对reloadData进行交换的,_cmd 传过来的就是新的方法名
Class baseClass = dzn_baseClassToSwizzleForTarget(self);
//类名和方法名组成一个key
NSString *key = dzn_implementationKey(baseClass, _cmd);
//查看这个key有没有对应的信息
NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key];
NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey];
//这里正常情况下获取的是原生reloadData方法的IMP指针
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);
}
}
上面的操作只有一个目的:看有没有人篡改reloadData的_cmd,表里存的是以@“UITableView_reloadData”为key的信息,如果_cmd被改变,这个key也会变,查不到对应的信息。
[self dzn_reloadEmptyDataSet] 函数
- (void)dzn_reloadEmptyDataSet
{
//dzn_canDisplay里面是判断当前类是否是UITablVeiw或者UICollectionView类,是否遵循了DZN的代理方法,不遵循不满足则返回
if (![self dzn_canDisplay]) {
return;
}
//dzn_itemsCount返回一个NSInteger类型,里面是对原生代理方法的拦截,比如获得sections个数,items个数;只有等于0的时候才显示空白布局
//下面操作就是什么时候开始布局,怎么布局等
if (([self dzn_shouldDisplay] && [self dzn_itemsCount] == 0) || [self dzn_shouldBeForcedToDisplay])
{
// Notifies that the empty dataset view will appear
[self dzn_willAppear];
DZNEmptyDataSetView *view = self.emptyDataSetView;
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 > 1) {
[self insertSubview:view atIndex:0];
}
else {
[self addSubview:view];
}
}
......
}
以上就是DZN的核心代码部分,主要是C函数定义IMP检查_cmd有没有被修改和查表建表的设计。具体的属性添加,方法绑定,方法拦截等设计有时间了再写出来。
如果有写的不当的地方,还请大佬们多多指教。