本文基于移动端动态化方案在知乎原生推广落地页「知乎画报」上的实践经验,对该方案技术升级过程中的思考以及技术关键细节做了详尽的解读。
商业化是互联网公司发展的重要阶段,App 端的商业广告业务对移动端动态化能力的需求很强烈,一方面需要灵活的样式调整和 AB 实验提升广告 CTR;另一方面提升功能的上线效率,新的产品功能可以快速的触达用户。基于此商业移动端团队从 2018 年 Q2 开始搭建动态化基础能力,逐步实现了 App 内各个位置广告卡片的动态化开发和上线(为了便于沟通和描述,我们给这个方案起了个比较「动态」的名字:Morph)。
广告推广落地页「Landing Page」是商业化广告的重要组成部分。做商业化广告的同学可能都处理过落地页优化的问题,「落地页打开速度」、「落地页到达率」、「落地页转化效果」几个指标的提升始终是 App 端落地页体验优化的重要挑战。
为解决上述业务痛点,提供更好的客户体验, 2018 年末商业产品提出在知乎 App 中支持原生推广落地页——「知乎画报」的业务需求,落地页内可使用图片、文字、视频等富媒体形式承载原生内容。此时动态化基础能力(Morph)已经基本成熟,对于当时提出的业务需要,我们自然而然的想到了复用 Morph 方案,同时在其基础能力上进行技术升级,将原有的信息流广告动态化扩展为全面的广告动态化。
从业务角度分析,不同于信息流广告卡片入口,落地页作为广告主体内容的承载容器,在呈现方式上更加开放,内容也更丰富;同时,用户对于广告内容的浏览与操作,广告主对于广告效果的获知、转化信息的采集,也都需要依托广告内容落地页来实现。在此过程中,相比于目前第三方 H5 落地页,采用原生落地页在工程能力上需要有如下提升:
1. 「加载速度更快,到达率接近 100%」。「到达率」在这里特指用户点击广告卡片到落地页完全加载的比例,是衡量落地页性能的重要指标。H5 落地页加载速度缓慢,「加载菊花」常常被人诟病,在网络不稳定或服务器异常的情况下有很大几率会导致页面加载缓慢甚至加载失败。除了严重影响用户体验之外,也极大的降低了广告转化率,对广告资源造成浪费;
2. 「支持预加载,实现秒开」。在第三方 H5 落地页,很难进行可靠的内容预加载。第三方服务器性能不一,性能较差的服务器很难承载大规模页面访问带来的服务压力;
3. 「转化信息的全面采集,用户体验升级」。对于很多广告主来说,由于没有自建站平台,只能使用第三方建站平台生成网页,显示效果参差不齐,内容质量难以把控,甚至相关数据的统计与分析也不得不依赖第三方平台,无论是数据准确程度还是学习成本都难以令人满意。对此,新方案在提供全面的转化信息同时,需要提供更低的学习成本与更好的用户使用体验;
4. 「支持样式实验,转化效果可提升」。和广告卡片提升 CTR 一样,通过进行落地页样式实验可以有效并持续地提升转化效果。
因此,对于原生推广落地页的动态化能力升级,成为了整个动态化进程中不可或缺的一环。(同样,为了便于沟通和描述,我们给这个方案起了个名字:Canvas)。
下面从「技术升级选型」、「知乎画报业务流程」、「技术实现细节」三个方面介绍一下此次技术方案改造升级。
有哪些技术选型上的升级,如何考量的?
移动端已实现的动态化基础能力(Morph)主要面向信息流广告卡片的展现,基于 Flexbox 布局编写的样式文件集合保存在云端样式数据库,通过独立的样式服务接口下发。信息流广告数据下发时会根据 App 本地已支持的样式文件子集匹配下发可展示的广告数据,App 端解析样式文件,绑定数据最终呈现广告卡片。
Morph 方案实现中进行了一系列的技术考量,在 Canvas 中针对「布局解析系统」和「样式文件」进行了改造升级,其他部分就不在本文中展开讨论了。
升级布局解析系统,匹配落地页的复杂场景
Morph 方案中从信息流场景出发,iOS 布局解析和视图渲染均基于 ComponentKit 实现,Android 布局解析则基于 google/flexbox-layout 实现,视图渲染基于系统原生控件。Canvas 落地页场景中,页面样式会更加复杂多变,同时需要容纳更丰富的、定制程度高的控件。因此在视图渲染上,直接使用系统原生方案是比较合适的选择。
Android 端布局解析框架 google/flexbox-layout 可以兼容系统原生控件,已满足升级要求。iOS 端既然选择了使用原生 UIKit 框架,那么我们在选择 Flexbox 解析方案时,可以只关注于 Flexbox 布局约束的实现,而无需要求第三方库对基础属性,例如背景颜色的支持。 尤其是想要在较短时间内实现对整体页面约束布局设置,学习成本不宜过高。同时,包体积是知乎 App 特别关注的基础体验,也需要考虑在内。
综合考虑以上几点,我们选择了更轻量级、业务吻合度更高的 YogaKit。
样式文件拆分,支持云端自定义视图布局
样式文件是云端控制 App 端样式展现的关键纽带。在 Morph 中,广告卡片的样式文件是由工程师预先编写,由使用方(广告主)选择后下发到 App 端,换句话说,Morph 的布局能力在广告卡片样式文件生成的时候就确定了。Canvas 在 Morph 的基础上将样式的布局能力进一步暴露给使用方,支持对落地页布局的个性化定制。
因此,与 Morph 中所有样式都写在同一个样式文件中不同,Canvas 样式文件分为:基础组件样式文件(基础样式)和布局样式文件(壳样式)。
- 基础样式
与 Morph 不同之处在于:基础样式不是最终的样式,只是落地页视图中基础组件的样式,需要结合壳样式共同生效;沿用 Morph 中的样式服务接口单独下发;每个基础样式都有默认样式属性值,会被壳样式中的属性覆盖。
- 壳样式
落地页的最终样式结构,像「壳」一样将基础样式包裹起来;决定落地页的视图布局,随广告数据下发;支持对基础样式属性进行覆盖更改。
「知乎画报」的业务流程设计
从用户视角上看,「知乎画报」的总体业务流程如下图:
总体流程下可以分为两个子流程:一、基础样式下发流程
- 基础样式存储在云端基础样式数据库;
- App 启动时,从样式服务获取当前版本的最新基础样式集合;
- 数据校验通过后,存入本地基础样式数据库。
二、落地页样式展现流程
- 以信息流为例,用户刷新信息流展现广告卡片,广告数据中包含了对应的落地页壳样式信息;
- 与基础样式数据合并解析,绑定数据,生成最终的样式信息;
- 根据样式信息,在用户浏览信息流时预先渲染原生落地页;
- 点击广告卡片,展现原生落地页。
同时,Canvas 与广告实验系统深度结合,支持对落地页样式进行细粒度的 AB 测试实验和数据收集,用于 CTR、CVR 提升实验。
下面我们来看一下为了实现上述业务流程,各参与角色做了哪些事情。
广告主需要做什么
广告业务前端为广告主提供专门的落地页建站工具平台。广告主在该平台将已支持的云端基础样式数据库中的基础组件,按照自己的需求进行简单的选择、拼接,组合成完整的落地页样式。然后上传对应的广告素材即可,操作过程非常简单。具体介绍可访问:这里。
后端做了什么
业务前端将制作好的页面解析生成 App 端可理解的壳样式数据,与广告素材关联后同步存储到广告业务数据库。当 App 端请求广告数据时,广告引擎根据 App 基本信息进行匹配,将满足条件的落地页壳样式数据封装在广告数据中一并下发。
App 端做了什么
App 在安装时会自带一个预埋了最新基础组件样式的本地基础样式数据库,如果云端更新了基础组件样式,可以通过样式服务进行更新。当用户刷新页面并请求到新的广告数据后,取出随广告同时下发的壳样式数据,与之前存储在本地的基础样式合并,生成落地页视图。(样式合并的细节将在后文详述。)
特别的,App 在广告卡片展现的同时,会提前预渲染广告落地页视图,在点击广告之后,落地页打开过程中不需要再次等待页面生成,实现秒开显示,提升落地页到达率。
相比于 H5 落地页对于 App 的黑盒,用户在落地页内的操作均属于对原生控件操作,浏览、填写信息、点击等行为均可收集用于支持 CVR 提升、oCPC 等工作。
有哪些值得关注的技术细节?
通过上文的叙述,我们已经对 Canvas 的技术基础和「知乎画报」的业务有了比较完整的认识。在 Canvas 动态化方案技术升级的过程中,移动端研发解决了很多工程设计和实现的问题,积累了宝贵的经验,接下来就把我们认为值得关注的技术细节做一个分享。
基础样式文件与基础组件生成
Canvas 广告落地页中,目前支持的所有组件类型包括:文本、图片、视频、勾选框、文字选择控件、图片选择控件、表单,其中表单内子组件包括文字输入框、下拉选择框。此外,还有两个容器组件:场景和分页。
基础样式列表
对于每一种基础组件样式,使用单独的样式名称 canvas_style 来标识,每一种样式名称会一一对应到字段 type
基础样式文件的数据存储分为两个部分:
- 云端基础样式数据库
基础样式以 JSON 的形式,与其对应的 Canvas 版本、客户端版本一并存储到云端数据库中。
- 本地基础样式数据库
App 启动时,访问样式服务接口,样式服务根据请求 Header 中的: App 平台、App 版本、 Canvas 版本,以及 App 端现有基础样式及版本信息集合,增量下发基础样式数据;
App 接收到新的基础样式数据后,会依次进行:Hash 校验、JSON 格式校验、实验信息的校验,校验通过后将接收到新的基础样式更新到本地数据库,同时进行对旧版本样式数据进行清理删除。
此外,为减小数据传输量,App 新版本发布时,会将已支持的基础样式数据预埋在数据库中。
最终生成的「知乎画报」页面与基础样式组件的对应关系如下图所示:
壳样式文件与样式合并
在上文的介绍中我们已经接触过基础样式和壳样式的概念。布局样式(即壳样式)随广告数据一起下发,和基础样式一起决定了落地页展现的最终样式。
对于一个控件来说,相关属性是通过样式模型进行配置的。本地基础样式数据库存储了每个组件最新的默认样式,在客户端收到随广告数据一起下发的壳样式数据后,会将服务器传回的壳样式数据与本地基础样式数据进行对比合并,最终形成包含了各组件相关属性配置、视图层级设置,以及 layout 约束设置的样式模型。
样式合并的过程示意如下:
在样式的合并过程中,遵循以下几点规则:
- 如果某个属性值,壳样式中没有设置而基础样式中有,则以基础样式中的值为准;
- 如果某个属性值,壳样式中和基础样式中都有设置,则以壳样式中的值为准;
- 如果样式中某个组件包含子组件,则依次遍历其中的每个子组件,对其执行该合并过程。
下图中我们将一个本地基础样式属性和壳样式属性合并成了最终样式模型:
整页布局与翻页处理
一个广告落地页的完整显示内容通常会包含图片、文字、表单等多种元素,很多场景下广告落地页全部显示内容会超过一屏。
上文中我们介绍过容器组件场景和分页。在 Canvas 落地页的视图层级中,最底层为一个总视图容器 canvas_scene,用来承载整个落地页内容, canvas_scene 中包含了至少一个 canvas_page,canvas_page 中包含一个 scrollView 视图,支持页面内组件的滚动展示。
canvas_page 的 Flex 布局规则如下:
Page { display: flex; flex-direction: column; justify-content: flex-start; align-items: center; }
canvas_page 支持两种整页布局方式,使用者可以根据具体诉求选择其一进行页面内容展示:
- 滑动布局:使用一个不分割的完整长页面,如下图 page one 所示;
- 翻页布局:使用多屏页面进行翻页显示,如下图 page two 所示。
- 滑动布局方式
如果广告主选择只使用一个 canvas_page 来显示内容,canvas_page 生成过程中,会循环遍历其中包含的所有子组件,对其进行数据绑定及约束设置,然后根据所有子组件的约束设置来适配自身包含的滑动视图的内容尺寸大小。以 iOS 为例,在 canvas_page 中包含了一个 UIScrollView,因此在计算出所有子组件约束后,将 scrollView 的 contentSize 设置成完整包括了所有子组件的容器的 size。这样,在这个页面上就可以通过上下滑动来浏览完整内容。
- 翻页布局方式
既然是「翻页」,也就代表底层容器中会包含多个 canvas_page,而每一个 canvas_page 的大小,可以等于或大于屏幕尺寸。(为了适配设备屏幕高度我们规定:页面内容大于一屏的情况下,分页组件根据内容高度自适应;当页面内容不足一屏时,按照一屏的高度展示,底部留白)。
在翻页方式布局中,当用户在滑动过程中即将切换到下一页时,会有一个 bounce 弹性动画来提示切换到下一页。
- 如何选择两种布局方式
通常对于广告主建站过程来说,如果所有落地页内容为一个整体,则建议使用滑动布局的方式将所有内容放置在同一页中;而如果整个内容分为数个部分,则可以分别将每个部分设置为一页,然后进行多页组合。
Canvas 的版本控制
从业务发展的角度来说,落地页中展示的组件不可能一次性全部支持,而是在业务迭代中对 Canvas 的基础样式集合进行动态更新和扩展的。因此,不同时期新增的组件,会对应不同的版本支持。一条广告数据也必须通过版本控制以确保下发到满足落地页展示条件的用户端。
Canvas 的版本规则可以分为三个部分来解释:
- 基础组件支持的 Canvas 版本
每个 Canvas 基础组件都会对应一个 Canvas 版本号(canvas_version),这个版本号写在基础样式云端数据库中,每当一个 Canvas 基础组件发生不向下兼容的修改时,提升对应的 canvas_version 。因此,一个基础组件在基础样式云端数据库可能有多条记录,对应多个 canvas_version。
- Canvas 版本兼容的最低 App 版本
每个 canvas_version 版本都会对应一个最低 App 版本,样式服务接口调用时,对于每个基础样式组件,会选择⽀持当前客户端版本的最新一条基础样式数据记录下发到 App。
- 当前 App 支持的 Canvas 版本
用户手机上的 App 以当前预埋或已下发的所有基础样式中,最高的 canvas_version 最为当前 App 支持的 canvas_version。调用广告数据下发接口时,广告引擎端选取落地页壳样式中调用的基础组件 canvas_version 小于等于 App 支持的 canvas_version 的广告数据下发。
其他技术细节
- 数据绑定
基础样式的主要功能是设置视图显示约束,是与广告内容数据完全独立的存在,其中只包含了与视图显示属性相关的设置,而没有添加真正要显示的内容。因此在壳样式中,还需要进行显示数据的绑定。
数据绑定部分的逻辑沿用了 Morph 的数据绑定方案:
- 数据体节点的占位符使用 <? 作为开始标识,?> 作为结束标识,数组直接使用数字下标指定其具体位置;例如:<?ads.0.creatives.asset.brand_name?>。
- App 在数据绑定时,直接与广告 JSON 数据中的字段映射,无需经过 Native Model 解析,实现 数据 Model 动态化。因此上述数据占位符对应的 JSON 数据子结构为:ads[0].creatives.asset.brand_name。
- 事件处理与控件联动
Morph 方案中的事件处理都包含在 action 字段中,如果有需要额外添加的参数,可以添加在 extra 参数上。
Canvas 落地页在此处沿用了 Morph 的结构,但是在不同组件之间的事件联动上做了升级。以表单组件为例:
部分表单信息支持用户授权后由系统自动填充,当用户勾选授权框时,需要在表单输入框中自动填充相关信息。实现过程要保证两个关键点:
1. 确保在页面中能够获取到联动的组件。一种可行的办法是设置局部唯一标识,添加 stringTag 标签的方式对控件进行「标记」,接收到的壳样式数据中指定需要联动的控件标识;
2. 确保事件能够进行正确完整的传递。当某一控件接收到点击事件时,将这个点击事件消息通过广播出来,监听了这一消息的组件会对事件进行响应。
- 转场动画的实现
对于一个广告落地页来说,「显示什么」固然是很重要的,而除此之外,「怎么显示」也是一门学问。尤其对于原生广告落地页,要想做到「提升用户体验」,酷炫且恰当的转场动画不失为一个讨巧的选择。Canvas 原生落地页从广告卡片为起点,最终展示为全屏页面。因此,我们选择了一种「卡片展开式」的转场动画作为页面过渡方式。
具体的实现方式是:在用户点击广告卡片时,获取到当前卡片相对于整个手机屏幕的位置坐标,并将该位置作为广告落地页的初始坐标和初始大小,落地页的最终位置坐标以及尺寸则等于手机屏幕。因此能够执行一个从起始位置到最终位置的展开动画。与此同时,在广告卡片当前位置覆盖一个与卡片样式完全一样的截图并执行缩小动画,从初始卡片位置向中心缩小直到消失,就得到了视频中的演示效果。
因为 Canvas 落地页是预渲染的,在执行动画过程中,不需要额外考虑页面的生成及生命周期。落地页的展开动画与卡片的缩小消失动画同时执行,也让原本枯燥的广告页面打开过程显得耳目一新。
写在最后Canvas 动态化能力升级 是一次很有意义的移动端动态化方案实践,通过这次升级不仅将基于页面组件的动态化能力打造的更加完善,覆盖更全面的应用场景;也很好的满足了「知乎画报」的业务要求,用户体验大幅提升。
落地页预渲染后实现秒开,到达率基本达到 100%,同时,相比于 H5 落地页对于 App 的黑盒,用户在落地页内的操作均属于对原生控件操作,转化打点追踪更加准确,能够细化到广告创意粒度,更好的支撑 CVR 提升、oCPC 等工作的开展。
我们的动态化方案开发人员不多,现有系统功能和技术方案一定还有很多不足之处,欢迎对移动端动态化方案感兴趣的同学在文章评论区一起交流分享知识、经验、见解。后续我们还会对动态化方案的基础能力建设部分进行更多的介绍,最新的动态化方案相关技术文章会在知乎技术专栏和预览小程序的「资讯」页面持续更新。
本文作者: @于鹏洋