骨架屏(Skeleton Screen Loading)也叫加载占位图,是一种在加载过程中提高用户体验的解决方案。在日常使用的APP中,也已经很常见了,比如下面这张图片,是京东的商品列表页面,加载中的页面展示效果:

可以看到除了页面中间的常用loading之外,每一个商品所在的行,都有一个灰色填充的占位图,这就是我们今天要分享的骨架屏。在网络请求的过程中,给用户一个更加优化的加载过程,就是看着好像马上要加载成功了一样😀。

无疑,骨架屏还是比较明显的帮助我们提升了用户体验,下面就思考一下骨架屏的具体实现方案。

骨架屏

实现方案的思考

就以上面京东这个截图为例,我们不难想到的第一中最直观的方案,就是自定义cell,给一个是否有真实网络数据可填充的状态,在没有数据时,cell中所有的UI控件,比如imageview,label,button等等,都显示为一个灰色背景的加载状态。

这样做当然可以实现骨架屏,当我们项目较小,功能业务线单一时,这也算是一种可以实施的方案,但是他也存在几个很明显的劣势:

  • 侵入性大,与业务耦合性强
  • 工作量大,需要每一个页面,分别实现
  • 难维护,散落在项目的各个角落
  • ...

尤其是当项目工程比较复杂时,这种方案可以直接pass了。

下面分享一种,经过我们团队调研,最终在项目中实践的一个骨架屏的实现方案-----TABAnimation

TABAnimated

截止到我写文的时间,GitHub上TABAnimated已经获得了2.4k的star,说明已经得到了业界不少开发者的认可。当然调研一个三方库的可用性,也不能光看star,还要在性能以及是否可以满足我们自己的需求等方面去考虑。

对比上面说的那种直观的实现方式的弊端,我们对于骨架屏的实现主要要考虑下面几个点:

  • 耦合性,方便集成和移除
  • 自动化程度,减少工作量
  • 三方库是否一直在更新维护

除此之外,还要考虑,使用骨架屏是否会带来额外的内存消耗,以及渲染的压力。带着这些问题,我们一起来看一下TABAnimated的实现步骤和实现原理。

实现原理

在往下看TABAnimated实现原理的时候,希望你已经简单的看过TABAnimated的源码和demo,以帮助你更好的理解下面要说的内容。这里还必须要表扬一下作者的demo写的很棒,基本包含了日常开发中会遇到的大部分情况,并且代码中的注释也写的很清楚,大大增加了可读性。
这里有一个iOS交流圈:891 488 181 有兴趣的都可以来了解,分享BAT,阿里面试题、面试经验,讨论技术,裙里资料直接下载就行, 大家一起交流学习成长!

下面先来了解一下TABAnimated的框架结构,整体结构基于面向协议进行了分层处理, 层与层之间通过协议通信,且不必关心对方的实现。

  • 控制层:由控制模型绑定参数、把握时机,向生产层输送生产任务
  • 生产层:生产层拿到生产任务,使用内置缓存机制、复用池、生产流水线进行生产
  • 加工层:加工层将已经生产好的产品进行加工。主要通过动画协议、暗黑模式协议、调整协议,而协议的控制权交由控制层。所以即便是加工层的任务,开发者也只需要聚焦于控制层开放的api。

上面贴两张图片,更形象的表现出结构的分层情况。

下面是各层通信所用的协议:

  • 控制层、生产层通信协议:TABAnimatedProductInterface

  • 生产层、加工层通信协议:TABAnimationManagerInterface、TABAnimatedChainManagerInterface、TABAnimatedDarkModeManagerInterface

  • 生产层对外协议:TABComponentLayerSerializationInterface

用途:开发者在自定制动画时,如果需要为骨架单元增加属性,可在此实现序列化

  • 加工层对外协议:TABAnimatedDecorateInterface、TABAnimatedDarkModeInterface
下面先贴一张类图,然后再去看看各层的主要类和他们的职责:

控制层

控制层主要类及其职责:

类名 用途
TABViewAnimated 用于UIView
TABTableAnimated 用于UITableView
TABCollectionAnimated 用于UICollectionView
TABFormAnimated TABTableAnimated和TABCollectionAnimated的公共基类
UIView+TABControlAnimation 骨架动画最上层的启动、结束接口

控制层通过生产协议TABAnimatedProductInterface与生产层通信, 控制层在需要的时机选择不同的生产方法,生产骨架屏。

开发者从控制层切入使用,我们直接跟踪一下源码,看看调用栈(以tableview调用为例):

这一层需要仔细阅读的源码,我认为是交换tableview代理和数据源方法的部分,交换在TABFormAnimated中实现。这是骨架屏实现的第一步,在加载骨架屏时,通过交换的代理和数据源方法,展示骨架屏生产出的骨架元素。

- (void)exchangeDelegateOldSel:(SEL)oldSel newSel:(SEL)newSel target:(id)target delegate:(id)delegate {

    if (![delegate respondsToSelector:oldSel]) return;

    Class targetClass  = [self class];
    Method newMethod = class_getInstanceMethod(targetClass, newSel);
    if (newMethod == nil) return;

    Method oldMethod = class_getInstanceMethod([delegate class], oldSel);

    #ifdef DEBUG
        class_addMethod([delegate class], newSel, class_getMethodImplementation([delegate class], oldSel), method_getTypeEncoding(oldMethod));
        class_replaceMethod([delegate class], oldSel, class_getMethodImplementation(targetClass, newSel), method_getTypeEncoding(newMethod));
    #else
        BOOL isVictory = class_addMethod([delegate class], newSel, class_getMethodImplementation([delegate class], oldSel), method_getTypeEncoding(oldMethod));
        if (isVictory) {
            class_replaceMethod([delegate class], oldSel, class_getMethodImplementation(targetClass, newSel), method_getTypeEncoding(newMethod));
        }
    #endif
}

以及TABAnimatedCacheManager中使用到的缓存策略,这也涉及到文章前面部分提到的,使用骨架屏是否会带来额外的内存消耗,以及CPU消耗。针对这两个问题,作者都做了相关的优化。

缓存的是通过映射机制生成的骨架屏单元管理对象TABComponentManager, 对该对象使用一个plist文件来解释。同时,通过计数的方式,逐渐筛选出该用户经常加载的骨架屏,提高缓存命中率。

下面是缓存流程图:

App启动后,读取沙盒中所有的TABAnimatedCacheModel文件,根据loadCount降序排列TABAnimatedCacheModel数组,并加载数组中前n个TABComponentManager到内存中,存储方式是全局字典(n默认为20)。

下面就是绑定视图,进入生产层了。通过TABAnimatedProductInterface协议中的绑定方法productWithControlView进行通信

- (UITableViewCell *)tab_tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    if (tableView.tabAnimated.state != TABViewAnimationStart) {
        return [self tab_tableView:tableView cellForRowAtIndexPath:indexPath];
    }

    TABTableAnimated *tabAnimated = tableView.tabAnimated;
    NSInteger index = [tabAnimated getIndexWithIndexPath:indexPath];
    if (index < 0) {
        return [self tab_tableView:tableView cellForRowAtIndexPath:indexPath];
    }

    Class currentClass = tabAnimated.cellClassArray[index];
    UITableViewCell *cell = [tabAnimated.producter productWithControlView:tableView currentClass:currentClass indexPath:indexPath origin:TABAnimatedProductOriginTableViewCell];
    return cell;
}

生产层

生产层主要类及其职责:

类名 用途
TABAnimatedProductInterface 生产协议
TABAnimatedProductImpl 生产协议的实现类
TABAnimatedProduction 产品,也是缓存的对象,是骨架的主体
TABComponentLayer 骨架元素单元
TABAnimatedProductHelper 生产流水的辅助类,主要用来分担TABAnimatedProductImpl的任务
TABComponentLayerSerializationInterface 骨架屏序列化协议,供自定制动画使用

生产层主要是操作Production,主要流程如下:

  1. 生成背景层Layer
  2. 递归遍历原视图子View,并生成对应的⻣架Layer
  3. 链式编程,调整⻣架Layer的大小
  4. 将生成的Layer绑定到Production
  5. 将生成好的Production缓存到本地

鉴于篇幅问题,就不再一一贴代码了,感兴趣的可以自己阅读源码。下面一段可能是你比较感兴趣的地方,就是生产层如何通过目标view,遍历其子view,生成骨架的。

- (void)_recurseProductLayerWithView:(UIView *)view
                               array:(NSMutableArray <TABComponentLayer *> *)array
                              isCard:(BOOL)isCard {

    NSArray *subViews;
    subViews = [view subviews];
    if ([subViews count] == 0) return;

    for (int i = 0; i < subViews.count;i++) {

        UIView *subV = subViews[i];
        if (subV.tabAnimated) continue;

        [self _recurseProductLayerWithView:subV array:array isCard:isCard];

        if ([self _cannotBeCreated:subV superView:view]) continue;

        // 标记移除:会生成动画对象,但是会被设置为移除状态
        BOOL needRemove = [self _isNeedRemove:subV];
        // 生产
        TABComponentLayer *layer;
        if ([TABAnimatedProductHelper canProduct:subV]) {
            UIColor *animatedColor = [_controlView.tabAnimated getCurrentAnimatedColorWithCollection:_controlView.traitCollection];
            layer = [self _createLayerWithView:subV needRemove:needRemove color:animatedColor isCard:isCard];
            layer.serializationImpl = _controlView.tabAnimated.serializationImpl;
            layer.tagIndex = self->_targetTagIndex;
            [array addObject:layer];
            _targetTagIndex++;
        }
    }
}

加工层

生产完成后,根据开发者的配置,还需要对骨架元素进行加工:

  • 异步调整管理协议 TABAnimatedChainManagerInterface:选择并执行异步调整回调,负责进一步调整骨架元素
  • 动画管理协议TABAnimationManagerInterface:对骨架元素进行动画周期的管理
  • 暗黑模式管理协议TABAnimatedDarkModeManagerInterface:对目标视图进行暗黑模式状态的管理

加工层对外可定制协议:

  • 异步调整回调 TABAdjustBlock:异步调整骨架元素属性的主体
  • 暗黑模式转换协议TABAnimatedDarkModeInterface:暗黑模式转换的主体
  • 动画协议TABAnimatedDecorateInterface:动画添加的主体
到此,TABAnimated的实现原理就介绍完毕了。下面贴一张整体骨架屏的工作流程图希望可以帮助你理解整个过程:

总结

本文记录了一次项目中接入骨架屏的流程,从前期的调研,到确定了使用TABAnimated,再去详细阅读源码学习,到最终接入项目开发,然后上线。

其实原理性的东西作者都写的很清楚了,我这里只是摘录和插入一点儿自己的理解。希望能对还没接触过骨架屏的同学有一些帮助。然后就是这个库本身的分层结构,以及用到的多线程处理,缓存处理等都值得我们去阅读学习。
文章到这里就结束了,如果你有什么意见和建议欢迎给我留言。有兴趣需要资料也可以加我了解详情。

作者:萌呆宝
链接:https://juejin.cn/post/6900850348026658829