2024年抖音:技术优化打造最佳创作体验,2024年最新美团三面之后多久联系

尾声

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究。

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。 整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

最后想要拿高薪实现技术提升薪水得到质的飞跃。最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

当你有了学习线路,学习哪些内容,也知道以后的路怎么走了,理论看多了总要实践的。

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

一、定义目标和评估方法


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

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

围绕创作体验,我们建立了一套完整的数据指标体系,包括用户使用过程中的各种重要性能和体验指标,例如拍摄/编辑首帧、卡顿、帧率、各种分支能力(音乐/道具/贴纸等)的面板加载时长/下载时长/下载成功率等等,同时构建了创作体验-> 投稿率 -> 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

Type: TEXT

*/

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> {

// 仅展示关键抽帧逻辑

@Override

protected CloseableReference getResult() throws Exception {

String path = getLocalFilePath(imageRequest);

Bitmap thumbnailBitmap = ThumbnailUtils.createVideoThumbnail(path, calculateKind(imageRequest));

return CloseableReference.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);

最后

愿你有一天,真爱自己,善待自己。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

fore exiting a runloop run

self.runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, activities, YES, INT_MAX, runLoopCallback)

CFRunLoopAddObserver(runLoop, self.runLoopObserver, kCFRunLoopCommonModes);

最后

愿你有一天,真爱自己,善待自己。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值