IMYAOPTableView 源码学习笔记

Head

近期由于入职了新公司,接触到了资讯业务的模块,看了看代码发现资讯业务的广告植入是由IMYAOPTableView 实现的,出于好奇,探索了下源码,走了一边流程,虽然框架中还有挺多东西没看懂[ :( ],这边先将流程记录下来

Content

IMYAOPTableView 总体是将业务流与广告流分开,再记录原数据源以及代理、新数据源以及新代理,最后再分发给对应的流 </br>
框架中的三行代码,对应的位置就是YYFeedListExample 中的 tableView:didSelectRowAtIndexPath:

UITableView *feedsTableView = [ctrl valueForKey:@"feedsView"];
self.aopDemo = [IMYAOPTableDemo new];
self.aopDemo.aopUtils = feedsTableView.aop_utils;

这里使用kvc取出业务流,精髓就在于设置aop_utils这个属性上,我们点击右边的aop_utils进入:

- (IMYAOPTableViewUtils *)aop_utils {
    IMYAOPTableViewUtils *aopUtils = objc_getAssociatedObject(self, kIMYAOPTableUtilsKey);
    if (!aopUtils) {
        @synchronized(self) {
            aopUtils = objc_getAssociatedObject(self, kIMYAOPTableUtilsKey);
            if (!aopUtils) {
                ///初始化部分配置
                [_IMYAOPTableView aop_setupConfigs];
                // 获取aop utils,设置aopUtils的tableView对象
                aopUtils = [IMYAOPTableViewUtils aopUtilsWithTableView:self];
                // 设置关联
                objc_setAssociatedObject(self, kIMYAOPTableUtilsKey, aopUtils, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                // 注入tableView
                [aopUtils injectTableView];
            }
        }
    }
    return aopUtils;
}

这里创建单例对象,使用runtime关联,没看懂[_IMYAOPTableView aop_setupConfigs];是干嘛用的,麻烦哪位好心人看懂了告诉我下....</br>
走进aopUtilsinjectTableView方法:

- (void)injectTableView {
    UITableView *tableView = self.tableView;
    // 记录原始的数据源以及代理对象
    // 这里如果你点Twitter 就是 :T1HomeTimelineItemsViewController
    _origDataSource = tableView.dataSource;
    _origDelegate = tableView.delegate;

    [self injectFeedsView:tableView];
}

这边把原数据源、原代理存储在aopUtils_origDataSource以及_origDelegate,这边也就是T1HomeTimelineItemsViewController 对象,再走进injectFeedsView方法:

- (void)injectFeedsView:(UIView *)feedsView {
    struct objc_super objcSuper = {.super_class = [self msgSendSuperClass], .receiver = feedsView};
    // 设置新的数据源以及代理对象: objcSuper结构体的地址即为第一个成员(receiver)的地址
    // objc_msgSendSuper 消息接收者还是 feedsView 只是查询方法是去父类查找
    // feedsView.delegate = self;
    // feedsView.dataSource = self;
    ((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];
    Class aopClass = [self makeSubclassWithClass:self.origViewClass];
    if (![self.origViewClass isSubclassOfClass:aopClass]) {
        [self bindingFeedsView:feedsView aopClass:aopClass];
    }
}

这里构造了一个结构体objcSuper,使用objc_msgSendSuper发送消息,个人感觉

((void (*)(void *, SEL, id))(void *)objc_msgSendSuper)(&objcSuper, @selector(setDelegate:), self);
((void (*)(void *, SEL, id))(void *)objc_msgSendSuper)(&objcSuper, @selector(setDataSource:), self);

等价于:

feedsView.delegate = self;
feedsView.dataSource = self;

接下来,走进makeSubclassWithClass

- (Class)makeSubclassWithClass:(Class)origClass {
    NSString *className = NSStringFromClass(origClass);
    NSString *aopClassName = [kA`setupAopClass`PFeedsViewPrefix, stringByAppendingString:className];
    Class aopClass = NSClassFromString(aopClassName);

    if (aopClass) {
        return aopClass;
    }
    aopClass = objc_allocateClassPair(origClass, aopClassName.UTF8String, 0);

    [self setupAopClass:aopClass];

    objc_registerClassPair(aopClass);
    return aopClass;
}

这里动态创建子类kIMYAOP_ClassName,并注入实现该子类方法的类为_IMYAOPTableView,覆盖父类的实现,比如进入到setupAopClass,查看

[self addOverriteMethod:@selector(reloadData) 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 {
    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);
    class_addMethod(aopClass, seletor, imp, types);
}

这里动态生成aop_seletor,并添加到子类kIMYAOP_ClassName的方法列表中:

class_addMethod(aopClass, seletor, imp, types);

所以当你再调用aopUtils.tableView.reloadData的时候,会走到_IMYAOPTableViewaop_reloadData方法实现,再往下看bindingFeedsView:aopClass:</br>
啊....这些是什么,不懂不懂,看懂的快告诉我....</br>

到这里,就配置好了原始的数据源、代理、动态创建子类、子类覆盖方法等,接下来就看广告类的设置

点击左边的aopUtils

 self.aopDemo.aopUtils = feedsTableView.aop_utils;

进入injectTableView

- (void)injectTableView {
    [self.aopUtils.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"AD"];

    ///广告回调,跟TableView的Delegate,DataSource 一样。
    self.aopUtils.delegate = self;
    self.aopUtils.dataSource = self;

    dispatch_async(dispatch_get_main_queue(), ^{
        [self insertRows];
    });
}

这里,就将aopUtils的代理设置成了广告类,用于最后的分发,往下看insertRows:

- (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);
    });
}

进入insertWithIndexPaths方法:

- (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;
}

原谅我比较懒,我直接看了结果,就是把广告的indexPath记录到sectionMap里把,嗯没错,应该是。。
最后就是调用过程了

[self.aopUtils.tableView reloadData];

会走到_IMYAOPTableViewaop_reloadData方法实现

- (void)aop_reloadData {
    AopDefineVars;
    aop_utils.isUICalling += 1;
    AopCallSuper(@selector(reloadData));
    aop_utils.isUICalling -= 1;
}

这里会调用父类(YYTableView)的reloadData方法,YYTableView又调用了[super reloadData],所以最终是也是调用[UITableView]reloadData,即走到aop_utils的数据源方法上,查看IMYAOPTableViewUtils+UITableViewDataSourcenumberOfRowsInSection方法,核心方法就在于

NSIndexPath *feedsIndexPath = [self feedsIndexPathByUser:[NSIndexPath indexPathForRow:rowCount inSection:section]];
- (NSIndexPath *)feedsIndexPathByUser:(NSIndexPath *)userIndexPath {
    if (userIndexPath == nil) {
        return nil;
    }
    NSInteger section = userIndexPath.section;
    NSInteger row = userIndexPath.row;

    ///转为table section
    section = [self feedsSectionByUser:section];

    NSMutableArray<NSIndexPath *> *array = self.sectionMap[@(section)];
    for (NSIndexPath *obj in array) {
        if (obj.row <= row) {
            row += 1;
        } else {
            break;
        }
    }
    NSIndexPath *feedsIndexPath = [NSIndexPath indexPathForRow:row inSection:section];
    return feedsIndexPath;
}

这里计算出了最终业务流+广告流的cell数量</br>
再往下看tableView:cellForRowAtIndexPath:方法:

- (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;
}

核心在于kAOPUserIndexPathCode:
这里区分该indexPath是广告流还是业务流,查看userIndexPathByFeeds,最终取出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;                                          \
    }
- (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;
}

END

还有很多地方没看明白,还需要多学习 :)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,188评论 1 23
  • 自己备忘,随便写 android网络框架源码解析及对比 android常用网络框架对比 Volley: 特点 基于...
    幻海流心阅读 1,453评论 0 4
  • iOS面向切面的TableView-AOPTableView 这个是公司很久之前的开源项目,一个大牛写的,在项目中...
    aron1992阅读 1,367评论 0 8
  • 姓名:王林锋 企业名称:三亚蔚蓝时代实业有限公司 组别:420期努力3组 【日精进打卡52天】 【知~学习、诵读】...
    shuaigefeng阅读 135评论 0 0