iOS面向切面的TableView-AOPTableViewgit
這個是公司好久以前的開源項目,一個大牛寫的,在項目中一直有在用,今天有空發了點時間看下如何實現,看了以後感受挺有收穫,故撰此文,分享給須要的同窗。
該庫的開源地址:MeetYouDevs/IMYAOPTableViewgithub
關於爲什麼使用AOP,在MeetYouDevs/IMYAOPTableView這個庫的簡介中已經有說起到了,主要是針對在咱們數據流中接入廣告的這種場景,最原始的方法就是分別請求數據以及廣告,根據規則合併數據,分別處理業務數據和廣告數據的展現這個流程以下圖所示。這種方案的弊端就是有很明顯的耦合,廣告和正常的業務耦合在一塊兒了,同時也違反了設計原則中的單一職責原則,因此這種方式是作的不夠優雅的,後期的維護成本也是比較大的。數組
dom
那麼如何解決這個問題呢?如何使用一種不侵入業務的方式優雅的去解決這個問題呢?答案就是使用AOP,讓正常的業務和廣告並行獨立滴處理,下圖就是使用AOP方式處理數據流中接入廣告流程圖ui
該如何設計一個可用AOP的TableView
呢?設計中提到的一點是沒有什麼問題是經過添加一個層解決不了的,不行的話就在添加一個層!
。AOP TableView
中一樣是存在着這個處理層的,承擔着以下的職責:一、注入非業務的廣告內容;二、轉發不一樣的業務到不一樣的處理者;三、處理展現、業務、廣告之間的轉換關係;另外還有一些輔助的方法。spa
下面這張圖是AOPTableView
設計類圖,IMYAOPTableViewUtils
該類就是這一層,爲了更加符合設計中的單一職責原則,經過分類的方式,這個類的功能被拆分在多個不一樣的模塊中,好比處理delegate
轉發的IMYAOPTableViewUtils (UITableViewDelegate)
、處理dataSource
轉發的IMYAOPTableViewUtils (UITableViewDataSource)
,主要完成以下事務處理debug
AOP設置的時序圖如上圖所示,如下是對應的代碼,建立了IMYAOPTableViewUtils
對象以後,須要注入 aop class ,主要的步驟以下:設計
特別地:動態建立子類以及給動態建立的子類添加aop的方法,最終該子類型的處理方法會在 _IMYAOPTableView
類中,下面會講到 _IMYAOPTableView
類的用途3d
- (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;
}
複製代碼
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;
}
複製代碼
如上圖所示,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;
}
複製代碼
就先寫到這了,若是不妥之處敬請賜教