抖音:技术优化打造最佳创作体验

前言

本文侧重阐述的,是指标优化的流程,即如何在既有的业务目标下,按照科学的视角评估问题,经过一定的流程分析、分解问题,找寻合理方向和关键路径,最终解决问题,并给予收益评估。

一、定义目标和评估方法

首先确定我们的业务目标 —— 抖音创作体验业内领先,提升投稿率。

如何定义“创作体验”和世界范围内的“业内领先”,我们需要将建设可量化评估的数据指标,并提供切实可信的评估方案。

围绕创作体验,我们建立了一套完整的数据指标体系,包括用户使用过程中的各种重要性能和体验指标,例如拍摄/编辑首帧、卡顿、帧率、各种分支能力(音乐/道具/贴纸等)的面板加载时长/下载时长/下载成功率等等,同时构建了创作体验-> 投稿率 -> DAU/留存 的贡献链路。如何找到这些技术指标,并区分其优先级和锚定其在评估体系中的权重,我们会在章节二中详细讲述。

e6fc9d3f0582dbe2bd8a0fac97dcb8ec.png 图 1-1 数据指标体系

评估方案上,我们采用了线下对比评测+自动化的方案,采样线上机型分布,分高/中/低端机档位测试;然后将上述的一系列指标加权计算后得出一个体验评分。根据评分的数值来判定体验和业内高水平产品的相对差距。评估过程中还会包括稳定环境、异常数据剔除、参数权重调整等通常手段,这里不再赘述。

0e30ca6049a1a24d2a52c5e06223798d.png

图 1-2 竞品评测部分指标星图

二、目标分解和相关性分析

章节一中我们提到了大量的数据指标,这些指标是如何挑选,又是如何确认其和业务相关的呢?

首先,一部分指标来源于脑爆和用户体验反馈,一部分来自于其他业务的经验(比如电商类业务的首帧对于页面转化率的影响,视频类 APP 的帧率对于用户留存的影响)和指标的拆解。

例如对于投稿率提升的拆解,首先是数学拆分,类似电商类的成单拆解:

投稿率提升 = ∏(各步骤转化率提升) = 权限申请转化提升*页面1转化提升*...*发布成功率提升

然后是自下而上的化学分解,我们得到类似贡献链路:

首帧指标\帧率 ... -> 某页面转化率 -> 投稿率

这些指标和转化率都会体现在我们 AB 实验的过程中,例如某些实验由于功能渗透低、受其他因素(运营/商务/节日...)影响大,是没有显著投稿率提升贡献的,但是提升了部分环节的转化率,也可以体现出其对业务的收益,并支持全量上线。

然后,我们需要对这些指标做相关性分析来确认其对于投稿率的影响,从而挑选出真正影响用户体验的部分,并区分出优先级。

相关性是指两个变量因素的相关密切程度,需要注意的是相关性不代表因果关系。

d615e7f8bc9bec27c7242a8a799f14ce.png

图 2-1 阶梯式相关性分析(示意图,非真实数据)

如上是最基本的阶梯式相关性分析图,我们可以看到随着首帧时长的增加,页面转化率会一直走低,可以认为二者是具有相关性的,且在不同区间内相关密切程度不同。同时我们可以清晰的结合 DAU 分布看出,在 400~1000ms 区间,相关性非常高,DAU 占比大,优化该区段的收益会非常显著;其次在 1500~1800ms 区间,相关性非常高,DAU 占比一般,优化该区段的收益会比较显著。我们还可以根据线下测试的不同档位的优化效果来预估线上的业务收益率:

线上业务收益 = 折扣系数 *  ∑(区段相关性斜率*(优化后分档-优化前分档))

注:实际操作中,预估收益和线上收益之间一般存在一个 0.5~0.7 的折损,不同的指标折损不同;

f97af098284696adbccf82572eaa0085.png

图 2-2 分位式相关性分析(示意图,非真实数据)

分位式相关性分析稍微增加了下理解成本,但是实际应用和计算上更为清晰便捷;横坐标轴用分位数值(PCT 5/10/15...)替代了原来单位间隔递增的档位数值(100ms/200ms/300ms...),能更直接的计算出不同分段(DAU/分段数)带来的收益。

由于相关性不代表因果关系,在已确认了相关性的技术指标,且后续优化评估需要投入的资源要求非常高时,我们可以先用劣化实验确认下因果关系,甚至更准确的进行定量描述,以免过度投入却没有得到相应的收益。例如我们人为的制造某技术指标在某分段内由 a₁ 劣化到 a₂,通过 AB 实验验证线上业务指标由 P₁ 降低到 P₂,来反向推理假如技术指标在某分段内由 a₂ 优化到 a₁ ,线上业务指标会由 P₂ 提升到 P₁ 。

根据相关密切度、优化的收益预估和计算上工时后的投入产出比等数据,我们可以将已有的数据指标做一下优先级排序,作为后续优化项目排期的依据,并在竞品对比评测中给予不同的权重。

三、优化实战

3.1 Android 相册体验优化

  • 问题发现

相册是上传路径的第一个界面,业务场景非常重要,导入投稿占总投稿中的比例也很高,但是体验上存在较长时间的 loading 和封面加载缓慢的问题。

  • 优化方案

相册页原始的加载逻辑:

53317c1e687929b1218cf5e6bb0143c9.png

我们针对每一个环境都做了一系列的优化:

    • 使用 Scene 替换 Activity

相册页使用的 Activity 作为载体,但我们测试发现一个空的 Activity 在中端机上面加载耗时就需要 50ms 以上,所以使用字节跳动开源的 Scene 替换了 Activity。

    • 通过预加载/动态刷新来加载数据

如上图,我们可以看到我们相册显示前会有一个加载媒体数据的过程,所以我们针对这部分数据进行了一次预加载,由于抖音拍摄页右下角有一个相册的 icon(通过查询用户相册最新的图片),所以我们通过复用减少了一次数据的查询。

在媒体数据查询中我们可以从 XXXColumns了解到系统提供给我们的所有的信息字段,当我们进行数据查询时,应该尽量的精简我们的查询字段,因为字段数量的多少会直接影响我们的加载速度。

public interface MediaColumns extends BaseColumns {
    // 省略部分代码 ...
    /**
     * The MIME type of the file
     * <P>Type: TEXT</P>
     */
    public static final String MIME_TYPE = "mime_type";
    /**
     * The height of the image/video in pixels.
     */
    public static final String HEIGHT = "height";
 }

当用户进入相册页后,就优先使用缓存展示相册信息,然后在子线程查询后通过 RecyclerView 中的 DiffUtil 异步计算 Diff 后进行刷新。

    • Tab 懒加载和封面加载

抖音的相册页使用的 ViewPager,有三个 Tab,但是我们知道 ViewPager 会默认加载页面的左右两页的。通过源码发现,ViewPager 提供的 setOffscreenPageLimit也并不能解决预加载的问题。所以通过重写 PagerAdapter 来实现 Tab 没有显示出来时候不加载任何内容。

public void setOffscreenPageLimit(int limit) {
    // DEFAULT_OFFSCREEN_PAGES = 1
    if (limit < DEFAULT_OFFSCREEN_PAGES) {
        Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to "
                + DEFAULT_OFFSCREEN_PAGES);
        limit = DEFAULT_OFFSCREEN_PAGES;
    }
    if (limit != mOffscreenPageLimit) {
        mOffscreenPageLimit = limit;
        populate();
    }
}

抖音封面加载使用的是 Fresco,封面加载如果有合适的媒体库的缩略图,就优先使用缩略。如果没有媒体库的缩略图的情况下,我们针对 Fresco 中的对图片和视频封面的加载进行了一系列的优化。

图片封面: 由于现在的照片普遍质量比较高,很多都达到了 3-5M,对于一些中低端机,每次对原图进行 resize 也是比较耗资源的,所以我们采用了空间换时间,加载每一个图片时,给符合要求的图片缓存了一个 resize 后的图片,从而让中低端机的封面加载的更快。

视频封面: Fresco 对视频抽帧使用的是 MediaMetadataRetriever,且并没有对视频封面抽帧做缓存,导致每一次视频封面都需要重新抽帧,所以视频封面相当的慢。针对这个我们可以通过两个方式进行优化:

  1. 替换抽帧方式,可用多媒体相关库进行抽帧

  2. 对抽帧出来的封面进行磁盘缓存。

public class LocalVideoThumbnailProducer implements Producer<CloseableReference<CloseableImage>> {
  // 仅展示关键抽帧逻辑
  @Override
  protected CloseableReference<CloseableImage> getResult() throws Exception {
      String path = getLocalFilePath(imageRequest);
      Bitmap thumbnailBitmap = ThumbnailUtils.createVideoThumbnail(path, calculateKind(imageRequest));
      return CloseableReference.<CloseableImage>of(new CloseableStaticBitmap(
          thumbnailBitmap,
          SimpleBitmapReleaser.getInstance(),
          ImmutableQualityInfo.FULL_QUALITY, 0));
  }
  • 其它优化

除上面的优化以外,我们还做一系列的探索: 产品形态优化(如 ins 相册只有 9 张图);厂商合作优化(手机厂商有封面的缓存私有接口,获取封面的 Bitmap 非常快);多个 Tab 复用 RecycledViewPool;低端机切 Tab 时主动释放 释放 Fresco Request;AndroidQ 适配等一系列的优化,由于篇幅有限,就不赘述了。

优化后相册页的加载逻辑:

1bb95fb26e8349e87f6a0ccdedc8ed9d.png
  • 效果收益

线下测试:

测试条件:

  • vivoNEX

  • 8k 张图、665 视频

优化前

409c8ec3ce47dfce4a5b4ebe1f802bd6.gif

优化后

7d710f9eafb8fa8eb1764d31a10685bf.gif

线上数据:相册首帧 PCT50 -43%;投稿率、开拍率、人均上传投稿数提升显著;

3.2 iOS 组件性能架构优化

  • 问题发现

近一年来创作链路代码组件化逐步落地和优化,已形成了一套兼顾扩展性和隔离性的组件化方案,但是由于组件的加载仍然是主线程串行执行,所有功能性组件的加载仍然在 page load 时机进行,这样就导致了首帧需要等待所有组件加载完成才能执行的问题,并且随着后续业务组件的添加,这个问题将愈发严重。

Page Ready:整体组件加载过程完成,页面处于可正常交互响应状态;组件加载通常执行如下操作:UI Load、缓存读取、发起网络请求、RX 信号绑定等。

658821e0e0455ba541d9b7a18122078c.png

图 3-1-1 首帧被组件加载阻塞

解决方案一:延迟加载组件

2fd89e80a56f820cfb88a69502c5b2b8.png

图 3-1-2 组件加载延迟到首帧之后

为了解决问题,我们第一个想到的办法就是将组件的加载操作延后到首帧之后执行,同时为了保证首屏 UI 的正常加载,我们将组件加载拆分为首屏 UI+耗时操作两个部分,效果也是立竿见影的,首帧的时长得到了大幅度的削减,并且后续新增组件也不会对首帧产生影响。但是新的问题又出现了,由于组件加载的延后 ,一部分跟业务相关的事件绑定操作(组件间通信通过事件传递)也同时被延后,这样整体 Page Ready 的时长被拉长,感知上用户进入页面后并不能立刻点击相关功能按钮,需要等待首帧的结束,线上核心功能的渗透率小幅下降,对于创作场景来说是不能接受的。

解决方案二:组件加载和首帧同时进行

e59e949376c390f8b78c5efb3a18a719.png

图 3-1-3 首帧和组件加载同时进行

有了方案一的铺垫,我们只需要解决一个关键问题:组件的加载也不能够被首帧 delay。看起来我们得到了一个伪命题 —— 组件加载不能阻塞首帧、首帧也不能阻塞组件加载,因为两者都是主线程串行执行,肯定是要有先后关系的,所以根本没法做到。

等等,好像我们忽略了一个重要问题,组件加载是逐个进行的,其实我们只需要保证首帧不被全量组件的加载阻塞就可以,毕竟单一组件的加载还是很快的。iOS 端我们想到了通过 runloop 的机制解决这个问题,既然我们要拆分组件的加载时机,倒不如干脆让组件加载在每一次 runloop 的空闲时机去执行,这样当有首帧渲染任务到来时,就可以优先执行首帧渲染,待 runloop 再次空闲时去执行剩余的组件加载,实现互相不阻塞的效果。关键实现如下:

- (void)registerTransactionMainRunloopObserver {
    AssertMainThread();
    if (self.runLoopObserver) return;
    __auto_type runLoopCallback = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        AssertMainThread();
        [self loopTransaction];
    };
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping
                                kCFRunLoopExit);          // before exiting a runloop run
    self.runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, activities, YES, INT_MAX, runLoopCallback)
    CFRunLoopAddObserver(runLoop, self.runLoopObserver, kCFRunLoopCommonModes);
}
  • 收益

    • 首帧时长:由于我们把耗时的组件加载操作剥离以及开拍的调用提前,最终实现了拍摄首帧 30~50%的优化幅度(分机型和分位数指标);

    • 性能防劣化:随着组件性能架构优化的最终落地,我们对于首帧的管控更加的严格,新业务的迭代再也不会影响首帧的时长,占用我们大量人力去排查线上首帧劣化的场景也少了很多,真正实现了首帧的代码级防劣化。

3.3 小屏幕的极致画质体验

  • 背景

手机屏幕小、计算资源有限、网络可靠性差(比如在地下室或者车站)、部分机型易发热的特点,使得移动设备很难在画质体验上与 PC 端相比较。如何在有限的条件下,尽量提升移动端画质体验,尤其是作为源头的视频投稿画质体验,是必须攻克的难题。为了解决这个问题,我们在硬件能力使用、编码参数调优、画质算法开发、产品策略的智能化上做了非常多的尝试,提出了一整套相对完整的解决方案。

  • 解决思路

硬件能力、画质算法、产品策略是移动端提升画质的三个主要途径。

6b790ccfd3ca19cfae71a9e20b1e8f93.png
  • 解决方案:硬件能力

硬件能力包括手机厂商为提升画质提供的基础能力,拍摄流程从前到后包括 Camera 能力、编解码能力、画质增强能力等。

  • Camera:防抖、自动曝光、夜景模式

  • 编解码:H264、bytevc1 硬编硬解能力

  • 画质增强:HDR

48aed0736ef4922172cb5d92a698cd34.png

解决方案:画质算法

通过算法提升画质是一种实现无限可能的手段,包括超分、画质增强、降噪、插帧、调色等。

4278407c2769e94782b181e6c19bc07a.png

解决方案:产品策略

全屏拍摄、高清、分级策略下发、视频透传等策略,通过产品设计,最大程度利用了屏幕、Camera 采集、芯片、内存等硬件资源,通过软件策略的合理调度和精心设置,使得用户感官上的画质体验提升,主观上达到最满意的状态。

  • 全屏拍摄:摄像头画面最大程度撑满屏幕 。

a039bf54794f0cd7da23579b7b20d1fb.png

图 3-3-1 - 全屏拍摄 - 系统相机 VS APP 相机

  • 高清:提升分辨率和码率是提升画质有效的途径。

32cbf8e9e5110adf93824e5826857020.png

图 3-3-2 - 分辨率\码率提升对比

  • 分级策略下发:对不同能力的机型下发不同的策略。

daa6e47cf20ebafa46c29bb4e697f908.png
  • 视频透传:将用户相册中的文件高清文件不经过客户端转码,而直接上传,完整保留文件的高画质。

310ce168bbaca38c7189987e7be5d44b.png
  • 画质评测:对多种场景进行画质评测,调整策略达到主观最优。

adb2b3d78f29a18f8ec86fb440dc4ca8.png
  • 分版本监控画质变化

40c6215cfe99f2e1fc2d56f6c8c0ad14.png
  • 收益

线上投稿/消费画质提升;高粉用户(万粉以上)投稿率提升非常显著,低粉用户投稿波动;相关投稿的消费时长提升明显。

四、监控和防劣化

日常数据采集和线上报警是我们监控线上性能和业务是否正常的重要手段,合理的报警阈值设置有助于我们及时发现线上问题,合理的采集和多维度分类有助于我们快速的定位问题并辅助分析解决。

举例来说,假如某省份的某 CDN 发生故障,或者某国家的网络异常,导致该节点的道具下发失败率&耗时增加,最终业务表现的可能是整体投稿率下跌。那么我们反推一下,在线上出现投稿率下跌的时候,我们应该依赖哪些业务和性能埋点,才能快速的定位到出问题的是哪个功能和哪个地区呢?这样反向思考的方式,也是构建埋点和监控体系的常见方法,这里就不展开去讲了。

相对优化而言,防劣化是很多 APP 研发流程中不够重视的一环,我们经常为了某些功能的快速上线和发版忽略了新代码带来的性能劣化,导致“有人填坑,有人挖坑”,“刚做了优化,又要做优化”。事实上,防劣化是和优化同等重要的工作。

针对章节一中的指标体系,我们建设了一套完整的防劣化体系,包括 P0 级别的 MR 防劣化,P1 级别的 Daily Check 和兜底的 Version 对比:

5e341d02438b89c43258348ea5118e90.png

图 4 -1 防劣化体系

五、常用的外部工具

  • 通用(内存/cpu/网络/时延...):TraceView、Android profiler、DTrace、Instruments、SignPost、Hook;

  • 页面渲染/帧率:Profile GPU rendering、systrace;

  • 页面绘制/布局层次:Hierarchy Viewer ;

  • 内存/CPU 分析:Memory Analyzer Tool、LeakCanary、System Trace。

六、凶悍的内部平台

工欲善其事,必先利其器,所谓君子生非异也,善假于物也。字节内部的大量高效平台为我们的工作提供了极大的便利,也让一些原本看似不可能的想法变为现实。

  • 自动化测试和 AI 识别等工具的加入,可以让原本要双月一次的 QA 手测竞品变更为周级对比测试,更精准的排除其他因素干扰,快速的捕捉竞品动态;

  • ByteBench 提供设备性能和兼容性数据,实现设备维度的策略分级;

  • 高效的 AB 实验平台可以小时级观察线上数据变化,灵活变更线上策略;

  • 画质和模拟实验室让我们可以主观评测不同用户的投稿质量,同时模拟不同的投稿场景,评测相关性能;

  • 海外机架平台可以远程部署研测(在安全合规允许范围内),根据不同国家和地区的实际网络场景,智能选择相关策略。

七、欢迎来和我们一起,挑战世界

2020 年中,我们提出了抖音创作体验优化的路线图,彼时只有五六位同学参与这个项目;20 年底,我们又提出创作体验业内领先的目标,项目内部也有争论,认为从入口场景、代码和功能复杂度等各方面看,这个目标都困难重重,还好经过团队不懈努力和精诚协作,最后都完成了既定目标。

当时我们周会聊过这样一段话:我们始终坚信,抖音能成为世界一流的移动互联网产品,就应该做出业内领先的技术性能体验,我们也要成长为世界一流最优秀的移动互联网研发工程师。

如果你有一样的信念,欢迎加入我们一起,挑战世界。

3c0904daa091cbb681acb54bbc2498c5.png

欢迎自带简历发送邮箱:双端性能优化/视频编辑/自动化 北京/杭州职位开放,maguangliang@bytedance.com;音视频引擎 北京/深圳/广州/上海/杭州职位开放,gongyun@bytedance.com;或扫描下方二维码投递。right here waiting 4 u ~

fe82e0e3eb5e1e373297b57a65785f61.png

双端性能优化/视频编辑/自动化

c5f941f56f3134907856cae536a7eba1.png

音视频引擎

Authored by: 字节跳动智能创作 - 创作工具 & 音视频 联合出品。

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值