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。app

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。

靜態類結構框架

 

相關文章
相關標籤/搜索