RumTime实践之--UITableView和UICollectionView缺省页的实现
有关RunTime的知识点已经看过很久了,但是一直苦于在项目中没有好的机会进行实际运用,俗话说“光说不练假把式”,正好最近在项目中碰到一个UITableView和UICollectionView在数据缺省的情况下展示默认缺省页的需求,这个时候RunTime大展拳脚的时候就到了。
大致的实现思路是这样的,因为UITableView和UICollectionView都是继承自系统的UIScrollView,所以为了同时实现UITableView和UICollectionView的缺省页,我们可以通过对UIScrollView进行扩展来达到这一目标。UITableView和UICollectionView都可以通过dataSource设置它们的数据源,而我们需要做的就是在UITableView和UICollectionView设置数据源之后去获取它们的数据源,从而得知当前UITableView和UICollectionView是否处于缺省状态。当UITableView和UICollectionView处于缺省状态时,我们就将加载缺省页视图。
1 #import <UIKit/UIKit.h> 2 3 @protocol EmptyDataViewDataSource; 4 5 @interface UIScrollView (EmptyData) 6 7 @property (nonatomic, weak) id <EmptyDataViewDataSource> emptyViewDataSource; 8 9 @end 10 11 @protocol EmptyDataViewDataSource <NSObject> 12 13 - (UIView *)emptyDataCustomView; 14 15 @end
首先实现一个UIScrollView的Category,在这个Category中声明一个EmptyDataViewDataSource的代理。当然,由于Category是在编译期间进行决议的,而OC的类对象在内存中是以结构体的形式排布的,结构体的大小是不能动态改变的,因此Category是不能动态地添加成员变量的,但是我们可以通过runtime中的
objc_setAssociatedObject和objc_getAssociatedObject方法去创建一个关联对象,从而在外部看起来好像我们为一个Category添加了一个成员变量。
我们自定义EmptyDataViewDataSource的set和get方法。创建对象ProtocolContainer,并将它关联到self上。之后执行SelectorShouldBeSwizzle方法,对系统方法进行替换。
1 - (void)setEmptyViewDataSource:(id<EmptyDataViewDataSource>)emptyViewDataSource 2 { 3 ProtocolContainer *container = [[ProtocolContainer alloc] initWithProtocol:emptyViewDataSource]; 4 objc_setAssociatedObject(self, kEmptyDataDataSource, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 5 6 [self SelectorShouldBeSwizzle]; //在EmptyViewDataSource的set方法中实现系统方法的替换操作 7 } 8 - (id<EmptyDataViewDataSource>)emptyViewDataSource 9 { 10 ProtocolContainer *container = objc_getAssociatedObject(self, kEmptyDataDataSource); 11 return container.protocolContainer; 12 }
在SelectorShouldBeSwizzle方法中不论是UITableView还是UICollectionView我们都需要替换系统的reloadData方法。由于UITableView还有插入,删除等操作,在这些操作后系统会自动去调用UITableView中的endUpdates方法,因此我们还需要替换endUpdates方法。
1 /** 2 需要被替换的系统方法 3 */ 4 - (void)SelectorShouldBeSwizzle 5 { 6 [self methodSwizzle:@selector(reloadData)]; //替换系统的reloadData实现方法 7 8 if ([self isKindOfClass:[UITableView class]]) //由于tableView有Cell的插入,删除实现,需要替换tableView的endUpdates方法,在操作结束后判断是否为空 9 { 10 [self methodSwizzle:@selector(endUpdates)]; 11 } 12 }
方法替换的实现主要依赖于runtime中的class_getInstanceMethod和method_setImplementation方法。我们先来看看class_getInstanceMethod方法的描述
1 /** 2 * Returns a specified instance method for a given class. 3 * 4 * @param cls The class you want to inspect. 5 * @param name The selector of the method you want to retrieve. 6 * 7 * @return The method that corresponds to the implementation of the selector specified by 8 * \e name for the class specified by \e cls, or \c NULL if the specified class or its 9 * superclasses do not contain an instance method with the specified selector. 10 * 11 * @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not. 12 */ 13 OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name) 14 OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);
从中我们可以看到这个方法的返回值是一个Method,那么什么是Method呢?Method是一个指向objc_method类型的结构体的指针,这个结构体中有三个成员变量,SEL类型的method_name(我的理解为方法的名称),char*类型的method_types(方法的参数)以及IMP类型的method_imp(方法的实现)。SEL和IMP是一种映射关系,OC在运行时通过SEL方法名去找方法对应的IMP实现,这也是为什么OC不能像C++那样实现函数重载的原因,因为在OC中方法名和实现是一一对应的关系,重名了OC就没办法去找对应的实现方法了(貌似有点扯远了。。。)现在我们就可以很好的理解class_getInstanceMethod这个方法了,这个方法实现的就是去Class cls中去找一个叫做SEL叫做name的方法, 并把这个方法返回。
1 /** 2 * Sets the implementation of a method. 3 * 4 * @param m The method for which to set an implementation. 5 * @param imp The implemention to set to this method. 6 * 7 * @return The previous implementation of the method. 8 */ 9 OBJC_EXPORT IMP method_setImplementation(Method m, IMP imp) 10 OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
现在再来看method_setImplementation方法就很好理解了。使用我们传入的IMP imp去替换Method中原来的IMP实现,并返回原来的IMP实现。在理解完了这两个方法以后我们再回到代码上来。
1 /** 2 替换系统方法的实现 3 4 @param selector 需要被替换的系统方法 5 */ 6 - (void)methodSwizzle:(SEL)selector 7 { 8 NSAssert([self respondsToSelector:selector], @"self不能响应selector方法"); 9 10 if (!_cacheDictionary) //如果缓存字典为空,开辟空间 11 { 12 _cacheDictionary = [[NSMutableDictionary alloc] init]; 13 } 14 15 NSString *selfClass = NSStringFromClass([self class]); //获取当前类的类型 16 NSString *selectorName = NSStringFromSelector(selector); //获取方法名 17 NSString *key = [NSString stringWithFormat:@"%@_%@",selfClass,selectorName]; //类型+方法名组成需要被替换的方法的key 18 19 NSValue *value = [_cacheDictionary objectForKey:key]; //查询方法是否被替换 20 21 if (value) 22 { 23 return; //方法被替换时,直接return 24 } 25 26 Method method = class_getInstanceMethod([self class], selector); 27 28 IMP originalImplemention = method_setImplementation(method, (IMP)newImplemention); //获取替换前的系统方法实现 29 30 [_cacheDictionary setObject:[NSValue valueWithPointer:originalImplemention] forKey:key]; //缓存替换前的系统方法实现 31 }
我们为乐不每一次都去执行方法的替换操作,我们使用一个缓存字典来存储已经被替换过的方法。我们使用类的类型和被替换掉实现的方法名组合起来作为缓存字典中存储的key值,value当然就是被替换下来的系统方法的实现。接下来我们看看我们在替换上去的newImplemention实现中做了什么。
1 /** 2 被替换后的方法 3 4 @param self self 5 @param _cmd 方法名称 6 */ 7 void newImplemention(id self, SEL _cmd) 8 { 9 NSString *selfClass = NSStringFromClass([self class]); 10 NSString *selectorName = NSStringFromSelector(_cmd); 11 NSString *key = [NSString stringWithFormat:@"%@_%@",selfClass,selectorName]; //使用当前的类名和当前方法的名称组合称为key值 12 13 NSValue *value = [_cacheDictionary objectForKey:key]; //通过key从缓存字典中取出系统原来方法实现 14 15 IMP originalImplemention = [value pointerValue]; 16 17 if (originalImplemention) 18 { 19 ((void(*)(id,SEL))originalImplemention)(self,_cmd); //执行被替换前系统原来的方法 20 } 21 22 if([self canEmptyDataViewShow]) //判断是否需要展示缺省页 23 { 24 [self reloadEmptyView]; 25 } 26 }
在OC中,每个方法被调用时,系统都会自动的为方法添加两个参数,一个self,另一个就是被调用的方法的名称,也就是我们的newImplemention中的两个入参。我们取出来系统原来的实现方法,这一步是必须的,毕竟我们的本意并不是抹除掉系统方法,而是在调用系统方法执行后判断是否需要展示缺省页而已啦。。。因此通过调用IMP的方式,直接调用系统方法实现。之后判断是否需要展示缺省页。
1 - (BOOL)canEmptyDataViewShow 2 { 3 NSInteger itemCount = 0; 4 5 NSAssert([self respondsToSelector:@selector(dataSource)], @"tableView或CollectionView没有实现dataSource"); 6 7 if ([self isKindOfClass:[UITableView class]]) 8 { 9 UITableView *tableView = (UITableView *)self; 10 id<UITableViewDataSource> dataSource = tableView.dataSource; 11 NSInteger sections = 1; 12 if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) 13 { 14 sections = [dataSource numberOfSectionsInTableView:tableView]; 15 } 16 if(dataSource && [dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) 17 { 18 for (NSInteger i=0; i<sections; i++) { 19 itemCount += [dataSource tableView:tableView numberOfRowsInSection:i]; 20 } 21 } 22 } 23 24 if ([self isKindOfClass:[UICollectionView class]]) 25 { 26 UICollectionView *collectionView = (UICollectionView *)self; 27 id<UICollectionViewDataSource> dataSource = collectionView.dataSource; 28 NSInteger sections = 1; 29 if (!dataSource || [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) 30 { 31 sections = [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]; 32 } 33 if (!dataSource || [dataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)]) 34 { 35 for (NSInteger i=0; i<sections; i++) { 36 itemCount += [dataSource collectionView:collectionView numberOfItemsInSection:i]; 37 } 38 } 39 } 40 41 if (itemCount == 0) 42 { 43 return YES; 44 } 45 else 46 { 47 return NO; 48 } 49 }
我们获取到对象本身,并从对象中取出它的数据源,然后判断数据源的item是否为0 ,当为0时,就可以自动展示缺省页了。
1 - (void)reloadEmptyView 2 { 3 UIView *emptyView; 4 if (self.emptyViewDataSource && [self.emptyViewDataSource respondsToSelector:@selector(emptyDataCustomView)]) 5 { 6 emptyView = [self.emptyViewDataSource emptyDataCustomView]; 7 } 8 else 9 { 10 emptyView = self.emptyDataView; 11 emptyView.backgroundColor = [UIColor redColor]; 12 } 13 14 if (!emptyView.superview) 15 { 16 if (self.subviews.count > 1) 17 { 18 [self insertSubview:emptyView atIndex:1]; 19 } 20 } 21 emptyView.frame = self.frame; 22 }
还记得我们在之前设置的EmptyDataViewDataSource吗?既然已经设置了EmptyDataViewDataSource,那我们就要把它充分利用起来嘛,来个自定义的emptyView也不是什么问题嘛~
源码github地址:https://github.com/cgy-tiaopi/CGYEmptyData