zoukankan      html  css  js  c++  java
  • DZNEmptyDataSet框架阅读

     
    前段时间使用公司封装的空白页占位视图工具,工具是对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]) {
            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 > 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:[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。

    静态类结构

  • 相关阅读:
    cocoaPod 使用
    iOS 7 开启侧滑效果
    数组排序
    JSON 保存到本地
    使用Android Studio时,Gradle同步失败的原因之一
    JDBC连接SQL Server数据库出现“通过端口 1433 连接到主机 localhost 的 TCP/IP 连接失败”错误解决办法
    tomcat启动命令行窗口出现乱码的解决方法
    eclipse中启动tomcat后,浏览器中出现404错误的解决办法
    Android Studio 出现Activity supporting ACTION_VIEW is not set as BROWSABLE错误
    解决AndroidStudio的错误:Installation failed with message Failed to finalize session : INSTALL_FAILED_INVALID_APK
  • 原文地址:https://www.cnblogs.com/zhou--fei/p/10805897.html
Copyright © 2011-2022 走看看