iOS面向切面的TableView-AOPTableView


關注公衆號【iOSSir】!看你想看,得你想得!

這個是公司很久之前的開源項目,一個大牛寫的,在項目中一直有在用,今天有空發了點時間看下如何實現,看了之後感覺挺有收穫,故撰此文,分享給需要的同學。
該庫的開源地址:MeetYouDevs/IMYAOPTableView

概覽

WHY AOP TableView

關於爲何使用AOP,在MeetYouDevs/IMYAOPTableView這個庫的簡介中已經有提及到了,主要是針對在我們數據流中接入廣告的這種場景,最原始的方法就是分別請求數據以及廣告,根據規則合併數據,分別處理業務數據和廣告數據的展示這個流程如下圖所示。這種方案的弊端就是有很明顯的耦合,廣告和正常的業務耦合在一起了,同時也違反了設計原則中的單一職責原則,所以這種方式是做的不夠優雅的,後期的維護成本也是比較大的。



那麼如何解決這個問題呢?如何使用一種不侵入業務的方式優雅的去解決這個問題呢?答案就是使用AOP,讓正常的業務和廣告並行獨立滴處理,下圖就是使用AOP方式處理數據流中接入廣告流程圖


HOW DESIGN 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中保存的數據如下,keysectionvalue是對應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作爲中間層承擔了作爲TableViewdelegatedataSource的職責,在改類中處理對應事件的轉發到具體的處理者:業務端或者是非業務的廣告端

比如下面的獲取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;
}

結束

就先寫到這了,如果不妥之處敬請賜教

iOSSir公衆號技術交流微信羣!
需要進羣可以添加公衆號助理“kele22558!”

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章