内容简介:iOS面向切面的TableView-AOPTableView这个是公司很久之前的开源项目,一个大牛写的,在项目中一直有在用,今天有空发了点时间看下如何实现,看了之后感觉挺有收获,故撰此文,分享给需要的同学。该库的开源库地址:
iOS面向切面的TableView-AOPTableView
这个是公司很久之前的开源项目,一个大牛写的,在项目中一直有在用,今天有空发了点时间看下如何实现,看了之后感觉挺有收获,故撰此文,分享给需要的同学。
该库的开源库地址: MeetYouDevs/IMYAOPTableView
概览
WHY AOP TableView
关于为何使用AOP,在 MeetYouDevs/IMYAOPTableView 这个库的简介中已经有提及到了,主要是针对在我们数据流中接入广告的这种场景,最原始的方法就是分别请求数据以及广告,根据规则合并数据,分别处理业务数据和广告数据的展示这个流程如下图所示。这种方案的弊端就是有很明显的耦合,广告和正常的业务耦合在一起了,同时也违反了设计原则中的单一职责原则,所以这种方式是做的不够优雅的,后期的维护成本也是比较大的。
 
那么如何解决这个问题呢?如何使用一种不侵入业务的方式优雅的去解决这个问题呢?答案就是使用AOP,让正常的业务和广告并行独立滴处理,下图就是使用AOP方式处理数据流中接入广告流程图

HOW DESINE AOP TableView
该如何设计一个可用AOP的 TableView 呢?设计中提到的一点是 没有什么问题是通过添加一个层解决不了的,不行的话就在添加一个层! 。 AOP TableView 中同样是存在着这个处理层的,承担着如下的职责:1、注入非业务的广告内容;2、转发不同的业务到不同的处理者;3、处理展示、业务、广告之间的转换关系;另外还有一些辅助的方法。
下面这张图是 AOPTableView 设计类图, IMYAOPTableViewUtils 该类就是这一层,为了更加符合设计中的单一职责原则,通过分类的方式,这个类的功能被拆分在多个不同的模块中,比如处理 delegate 转发的 IMYAOPTableViewUtils (UITableViewDelegate) 、处理 dataSource 转发的 IMYAOPTableViewUtils (UITableViewDataSource) ,主要完成如下事务处理
- 注入广告内容对应的位置
- 设置AOP
- 作为TableView的真实Delegate/DataSource
- 处理转发Delegate/DataSource方法到业务或者广告
- 处理delegate转发 ->IMYAOPTableViewUtils (UITableViewDelegate)
- 处理dataSource转发->IMYAOPTableViewUtils (UITableViewDataSource)

设置AOP

AOP设置的时序图如上图所示,以下是对应的代码,创建了 IMYAOPTableViewUtils 对象之后,需要注入 aop class ,主要的步骤如下:
- 保存业务的Delegate/DataSource ->injectTableView方法处理
- 设置TableView的delegate/dataSource为IMYAOPBaseUtils -> injectFeedsView方法处理
- 动态创建TableView的子类 -> makeSubclassWithClass方法处理
- 并设置业务的TableView的isa指针 -> bindingFeedsView方法处理
- 设置动态创建TableView的子类的aop方法 -> setupAopClass方法处理
特别地:动态创建子类以及给动态创建的子类添加aop的方法,最终该子类型的处理方法会在 _IMYAOPTableView 类中,下面会讲到 _IMYAOPTableView 类的用途
- (void)injectTableView {
UITableView *tableView = self.tableView;
_origDataSource = tableView.dataSource;
_origDelegate = tableView.delegate;
[self injectFeedsView:tableView];
}
#pragma mark - 注入 aop class
- (void)injectFeedsView:(UIView *)feedsView {
// 设置TableView的delegate为IMYAOPBaseUtils
// 设置TableView的dataSource为IMYAOPBaseUtils
struct objc_super objcSuper = {.super_class = [self msgSendSuperClass], .receiver = feedsView};
((void (*)(void *, SEL, id))(void *)objc_msgSendSuper)(&objcSuper, @selector(setDelegate:), self);
((void (*)(void *, SEL, id))(void *)objc_msgSendSuper)(&objcSuper, @selector(setDataSource:), self);
self.origViewClass = [feedsView class];
// 动态创建TableView的子类
Class aopClass = [self makeSubclassWithClass:self.origViewClass];
if (![self.origViewClass isSubclassOfClass:aopClass]) {
// isa-swizzle: 设置TableView的isa指针为创建的TableView子类
[self bindingFeedsView:feedsView aopClass:aopClass];
}
}
/**
isa-swizzle: 设置TableView的isa指针为创建的TableView子类
这里需要注意的是KVO使用的也是isa-swizzle,设置了isa-swizzle之后需要把设置的KVO重新添加回去
*/
- (void)bindingFeedsView:(UIView *)feedsView aopClass:(Class)aopClass {
id observationInfo = [feedsView observationInfo];
NSArray *observanceArray = [observationInfo valueForKey:@"_observances"];
///移除旧的KVO
for (id observance in observanceArray) {
NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
id observer = [observance valueForKey:@"_observer"];
if (keyPath && observer) {
[feedsView removeObserver:observer forKeyPath:keyPath];
}
}
object_setClass(feedsView, aopClass);
///添加新的KVO
for (id observance in observanceArray) {
NSString *keyPath = [observance valueForKeyPath:@"_property._keyPath"];
id observer = [observance valueForKey:@"_observer"];
if (observer && keyPath) {
void *context = NULL;
NSUInteger options = 0;
@try {
Ivar _civar = class_getInstanceVariable([observance class], "_context");
if (_civar) {
context = ((void *(*)(id, Ivar))(void *)object_getIvar)(observance, _civar);
}
Ivar _oivar = class_getInstanceVariable([observance class], "_options");
if (_oivar) {
options = ((NSUInteger(*)(id, Ivar))(void *)object_getIvar)(observance, _oivar);
}
/// 不知道为什么,iOS11 返回的值 会填充8个字节。。 128
if (options >= 128) {
options -= 128;
}
} @catch (NSException *exception) {
IMYLog(@"%@", exception.debugDescription);
}
if (options == 0) {
options = (NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew);
}
[feedsView addObserver:observer forKeyPath:keyPath options:options context:context];
}
}
}
#pragma mark - install aop method
/**
动态创建TableView的子类
*/
- (Class)makeSubclassWithClass:(Class)origClass {
NSString *className = NSStringFromClass(origClass);
NSString *aopClassName = [kAOPFeedsViewPrefix stringByAppendingString:className];
Class aopClass = NSClassFromString(aopClassName);
if (aopClass) {
return aopClass;
}
aopClass = objc_allocateClassPair(origClass, aopClassName.UTF8String, 0);
// 设置动态创建的子类的aop方法,真实处理方法是在_IMYAOPTableView类中的aop_前缀的方法
[self setupAopClass:aopClass];
objc_registerClassPair(aopClass);
return aopClass;
}
/**
设置动态创建的子类的aop方法,这里做了省略
*/
- (void)setupAopClass:(Class)aopClass {
///纯手动敲打
[self addOverriteMethod:@selector(class) aopClass:aopClass];
[self addOverriteMethod:@selector(setDelegate:) aopClass:aopClass];
// ....
///UI Calling
[self addOverriteMethod:@selector(reloadData) aopClass:aopClass];
[self addOverriteMethod:@selector(layoutSubviews) aopClass:aopClass];
[self addOverriteMethod:@selector(setBounds:) aopClass:aopClass];
// ....
///add real reload function
[self addOverriteMethod:@selector(aop_refreshDataSource) aopClass:aopClass];
[self addOverriteMethod:@selector(aop_refreshDelegate) aopClass:aopClass];
// ....
// Info
[self addOverriteMethod:@selector(numberOfSections) aopClass:aopClass];
[self addOverriteMethod:@selector(numberOfRowsInSection:) aopClass:aopClass];
// ....
// Row insertion/deletion/reloading.
[self addOverriteMethod:@selector(insertSections:withRowAnimation:) aopClass:aopClass];
[self addOverriteMethod:@selector(deleteSections:withRowAnimation:) aopClass:aopClass];
// ....
// Selection
[self addOverriteMethod:@selector(indexPathForSelectedRow) aopClass:aopClass];
[self addOverriteMethod:@selector(indexPathsForSelectedRows) aopClass:aopClass];
// ....
// Appearance
[self addOverriteMethod:@selector(dequeueReusableCellWithIdentifier:forIndexPath:) aopClass:aopClass];
}
- (void)addOverriteMethod:(SEL)seletor aopClass:(Class)aopClass {
NSString *seletorString = NSStringFromSelector(seletor);
NSString *aopSeletorString = [NSString stringWithFormat:@"aop_%@", seletorString];
SEL aopMethod = NSSelectorFromString(aopSeletorString);
[self addOverriteMethod:seletor toMethod:aopMethod aopClass:aopClass];
}
- (void)addOverriteMethod:(SEL)seletor toMethod:(SEL)toSeletor aopClass:(Class)aopClass {
// 这里的这个implClass在AOPTableViewUtils中为_IMYAOPTableView
Class implClass = [self implAopViewClass];
Method method = class_getInstanceMethod(implClass, toSeletor);
if (method == NULL) {
method = class_getInstanceMethod(implClass, seletor);
}
const char *types = method_getTypeEncoding(method);
IMP imp = method_getImplementation(method);
// 添加aopClass也就是创建的子类型kIMYAOP_UITableView的处理方法,真实处理方法是在_IMYAOPTableView类中的
class_addMethod(aopClass, seletor, imp, types);
}
_IMYAOPTableView 的职责是在业务端直接使用 TableView 对应的方法的时候,把业务的规则转换为真实列表的规则,比如下面的业务端调用了 cellForRowAtIndexPath 这个方法,会走到如下的方法中,这里的 indexPath 是业务自己的 indexPath ,比如在列表可见的第五个位置,但是前面是有两个广告,在业务端的逻辑中该indexPath对应的位置是在第三个位置的,所以需要进行修正,返回正确的 IndexPath ,获取到对应位置的 Cell ,这样才不会有问题
- (UITableViewCell *)aop_cellForRowAtIndexPath:(NSIndexPath *)indexPath {
AopDefineVars;
if (aop_utils) {
// 修复业务使用的indexPath为真实的indexPath
indexPath = [aop_utils feedsIndexPathByUser:indexPath];
}
aop_utils.isUICalling += 1;
UITableViewCell *cell = AopCallSuperResult_1(@selector(cellForRowAtIndexPath:), indexPath);
aop_utils.isUICalling -= 1;
return cell;
}
使用AOP
非业务数据插入
IMYAOPBaseUtils 类提供了两个方法用于非业务数据的处理
///插入sections 跟 indexPaths
- (void)insertWithSections:(nullable NSArray<__kindof IMYAOPBaseInsertBody *> *)sections;
- (void)insertWithIndexPaths:(nullable NSArray<__kindof IMYAOPBaseInsertBody *> *)indexPaths;
// 实现
- (void)insertWithIndexPaths:(NSArray<IMYAOPBaseInsertBody *> *)indexPaths {
NSArray<IMYAOPBaseInsertBody *> *array = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(IMYAOPBaseInsertBody *_Nonnull obj1, IMYAOPBaseInsertBody *_Nonnull obj2) {
return [obj1.indexPath compare:obj2.indexPath];
}];
NSMutableDictionary *insertMap = [NSMutableDictionary dictionary];
[array enumerateObjectsUsingBlock:^(IMYAOPBaseInsertBody *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
NSInteger section = obj.indexPath.section;
NSInteger row = obj.indexPath.row;
NSMutableArray *rowArray = insertMap[@(section)];
if (!rowArray) {
rowArray = [NSMutableArray array];
[insertMap setObject:rowArray forKey:@(section)];
}
while (YES) {
BOOL hasEqual = NO;
for (NSIndexPath *inserted in rowArray) {
if (inserted.row == row) {
row++;
hasEqual = YES;
break;
}
}
if (hasEqual == NO) {
break;
}
}
NSIndexPath *insertPath = [NSIndexPath indexPathForRow:row inSection:section];
[rowArray addObject:insertPath];
obj.resultIndexPath = insertPath;
}];
self.sectionMap = insertMap;
}
调用 insertWithIndexPaths 插入非业务的广告数据,这里插入的数据是位置
///简单的rows插入
- (void)insertRows {
NSMutableArray<IMYAOPTableViewInsertBody *> *insertBodys = [NSMutableArray array];
///随机生成了5个要插入的位置
for (int i = 0; i < 5; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:arc4random() % 10 inSection:0];
[insertBodys addObject:[IMYAOPTableViewInsertBody insertBodyWithIndexPath:indexPath]];
}
///清空 旧数据
[self.aopUtils insertWithSections:nil];
[self.aopUtils insertWithIndexPaths:nil];
///插入 新数据, 同一个 row 会按数组的顺序 row 进行 递增
[self.aopUtils insertWithIndexPaths:insertBodys];
///调用tableView的reloadData,进行页面刷新
[self.aopUtils.tableView reloadData];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@", self.aopUtils.allModels);
});
}
在demo中使用了如上的代码调用, sectionMap 中保存的数据如下, key 为 section , value 是对应 section 下所有插入数据的 IndexPath 数组, sectionMap 数据会用于处理真实数据和业务数据之间的映射

userIndexPathByFeeds 方法使用 sectionMap 处理真实 indexPath 和业务 indexPath 之间的变换
// 获取业务对应的indexPath,该方法的作用是进行indexPath,比如真实的indexPath为(0-5),前面插入了两个广告,会把indexPath修复为业务的indexPath,也就是(0-3),如果该位置是广告的位置,那么返回nil空值
- (NSIndexPath *)userIndexPathByFeeds:(NSIndexPath *)feedsIndexPath {
if (!feedsIndexPath) {
return nil;
}
NSInteger section = feedsIndexPath.section;
NSInteger row = feedsIndexPath.row;
NSMutableArray<NSIndexPath *> *array = self.sectionMap[@(section)];
NSInteger cutCount = 0;
for (NSIndexPath *obj in array) {
if (obj.row == row) {
cutCount = -1;
break;
}
if (obj.row < row) {
cutCount++;
} else {
break;
}
}
if (cutCount < 0) {
return nil;
}
///如果该位置不是广告, 则转为逻辑index
section = [self userSectionByFeeds:section];
NSIndexPath *userIndexPath = [NSIndexPath indexPathForRow:row - cutCount inSection:section];
return userIndexPath;
}
AOP代理方法回调

如上图所示, IMYAOPTableViewUtils 作为中间层承担了作为 TableView 的 delegate 和 dataSource 的职责,在改类中处理对应事件的转发到具体的处理者:业务端或者是非业务的广告端
比如下面的获取cell的代理方法 tableView:cellForRowAtIndexPath: ,首先会进行 indexPath 的修复,然后判断是业务的还是非业务的,然后使用不同的 dataSource 进行相应的处理,代码段有做了注释,详情参加注释的解释
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
kAOPUICallingSaved;
kAOPUserIndexPathCode;
UITableViewCell *cell = nil;
if ([dataSource respondsToSelector:@selector(tableView:cellForRowAtIndexPath:)]) {
cell = [dataSource tableView:tableView cellForRowAtIndexPath:indexPath];
}
if (![cell isKindOfClass:[UITableViewCell class]]) {
cell = [UITableViewCell new];
if (dataSource) {
NSAssert(NO, @"Cell is Nil");
}
}
kAOPUICallingResotre;
return cell;
}
// 宏定义的代码段,用户是判断该位置是否是业务使用的IndexPath,是的话返回业务的DataSource->origDataSource,否则返回非业务的DataSource->dataSource
#define kAOPUserIndexPathCode \
NSIndexPath *userIndexPath = [self userIndexPathByFeeds:indexPath]; \
id<IMYAOPTableViewDataSource> dataSource = nil; \
if (userIndexPath) { \
dataSource = (id)self.origDataSource; \
indexPath = userIndexPath; \
} else { \
dataSource = self.dataSource; \
isInjectAction = YES; \
} \
if (isInjectAction) { \
self.isUICalling += 1; \
}
// 获取业务对应的indexPath,该方法的作用是进行indexPath,比如真实的indexPath为(0-5),前面插入了两个广告,会把indexPath修复为业务的indexPath,也就是(0-3),如果该位置是广告的位置,那么返回nil空值
- (NSIndexPath *)userIndexPathByFeeds:(NSIndexPath *)feedsIndexPath {
if (!feedsIndexPath) {
return nil;
}
NSInteger section = feedsIndexPath.section;
NSInteger row = feedsIndexPath.row;
NSMutableArray<NSIndexPath *> *array = self.sectionMap[@(section)];
NSInteger cutCount = 0;
for (NSIndexPath *obj in array) {
if (obj.row == row) {
cutCount = -1;
break;
}
if (obj.row < row) {
cutCount++;
} else {
break;
}
}
if (cutCount < 0) {
return nil;
}
///如果该位置不是广告, 则转为逻辑index
section = [self userSectionByFeeds:section];
NSIndexPath *userIndexPath = [NSIndexPath indexPathForRow:row - cutCount inSection:section];
return userIndexPath;
}
结束
就先写到这了,如果不妥之处敬请赐教
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 容器管理方面的四大考量
- 最全面的推荐系统评估方法介绍
- 最全面的Navigation的使用指南
- Hackertarget:一款帮助组织发现攻击面的强大工具
- 测试Android应用程序的逆向方法和寻找攻击面的技巧
- 最全面的CQRS和事件溯源介绍 - Software House ASC
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Just My Type
Simon Garfield / Profile Books / 2010-10-21 / GBP 14.99
What's your type? Suddenly everyone's obsessed with fonts. Whether you're enraged by Ikea's Verdanagate, want to know what the Beach Boys have in common with easy Jet or why it's okay to like Comic Sa......一起来看看 《Just My Type》 这本书的介绍吧!