写点什么

爱奇艺移动应用优化之路

  • 2019-08-03
  • 本文字数:6218 字

    阅读完需:约 20 分钟

爱奇艺移动应用优化之路

引言

移动端的应用,对启动时间的体验常常“锱铢必较”,当应用承载了更多的内容的同时,一直保持”秒开”的启动速度,是需要技术团队的不断优化。


移动应用的特点是需求变化快,相应地,其代码更迭和变化也是快速的,如果多个版本的更新迭代都没有关注到 APP 的性能和质量,慢慢地 APP 可能会变得臃肿、迟钝。所以针对 APP 各个维度的数据需要持续统计及对比,性能相关的优化也需要不断跟进,才能保持并提高 APP 的性能和质量。


经过多年来对爱奇艺 APP 的性能及质量优化,在安装包大小、启动时间、性能、稳定性等方面总结了很多经验。目前公司业务不断增长,各业务也都发布了自己的 APP,为了能让各独立 APP 借鉴爱奇艺视频在性能及质量优化上的相关经验,建立了针对移动应用优化的工具箱。


具体从以下几个方面优化:


  • 安装包大小

  • 启动速度

  • 稳定性

  • 其他维度

一、安装包大小

安装包大小对于 APP 来说是一个非常重要的指标:


1、iOS8 对于 APP 的 text 段有 60MB 的限制


2、超过 200MB 的 APP 需要连接 WIFI 下载(之前是 150MB)


3、启动速度会受包中二进制大小的影响


4、除去商店中 APP 的简介、截图,很多用户都会关注 APP 的大小,尤其是使用空间为 8G、16G 的用户



安装包大小的优化,主要包含两大块:资源大小的优化和二进制大小的优化。


资源大小的优化相对来说比较简单,主要包括以下几个方面:


1、资源压缩


2、未使用、重复资源的删除


3、资源上云


  • 资源压缩


Xcode 的编译选项中,提供了 Compress PNG Files 及 Remove Text MetaData From PNG Files,但是由于 PNG 是无损压缩,经过 Xcode 压缩后的图片资源,依然很大。



那么 PNG 是否可以通过“有损”压缩,来缩小体积呢?答案是可以。通常 UI 同事出的图都是 32 位图片,32 位图相当于 Alpha 透明度+24 位图,总共可以显示 2^24=16777216 种颜色。图片颜色丰富固然好,但是一般情况下,APP 包中的图片色彩范围实际很小,按钮或者一些小图使用 8 位图肉眼完全看不出与 32 位图有什么区别。



中的 32 位与 8 位图的对比


因此团队使用 pngquant 对大多数的 32 位图进行了处理,将其转为 8 位图,并且使用 Zopfli 进行了压缩,这样整体的 PNG 图片资源大概被压缩了 70%左右。这里要注意,由于一些渐变背景的颜色覆盖范围较大,转为 8 位图颜色丢失较大,表现效果会差很多,所以这些图片要谨慎处理。


  • 未使用、重复资源的删除


随着版本迭代,一些功能可能会下线,有时相关的资源却没有及时删掉,有些功能由于开发者不同,可能部分相对通用的图片会有重复。这部分资源会随着时间的推移慢慢累积,及时发现并删除是非常必要的。


图片是否被使用,可以通过图片名字符串的匹配来判断。需要注意的是图片名拼接的情况(比如新手引导的功能,很容易出现图片名拼接数字的情况),在图片资源预处理的过程中,通常有两张以上图片有相同的前缀或后缀,可以将其相同的部分提取出来,作为字符串匹配的依据。


重复资源的情况一般会发生在一些按钮、或者背景图由同一个 UI 同事生成,发给了不同功能的开发人员。这部分图片的判定一般可以通过图片大小相同或相近(大小在 5%范围内)、分辨率相同进行初筛,然后再通过 magick compare 工具进行图片的对比,提取出相似度极大的即可。



最后工具输出的结果需要开发人员进行二次确认,确保不会有误删的情况出现。



  • 资源上云


资源上云可以有效减少包内资源,唯一要注意的是这些资源由于是 lazy load,所以比较适合层级较深的页面使用。图片逻辑可以封装为工具类,前端使用只是调用一个方法,具体哪些图片取本地、哪些图片走云的策略可以自由配置。此外,资源上云的另一个好处是可以很方便地实现动态换肤。



JS、表情库等资源可以通过打包预下载的形式,并增加更新策略,这样 RN 页面、WebView、表情库都不占用安装包大小。


二进制大小的优化相对来说比较困难,除去部分功能使用 Web 页面或者 RN,主要工作集中在无用类、无用代码的分析及删除。


Xcode 可以生成一个 Link Map 文件,文件包含了可执行文件的路径、目标文件、符号等各种信息。文件中__objc_classrefs 表示引用到的类,_objc_classname 表示 APP 内的类名,通过分析两者的差别,可以得出未使用的类,同理,分析__objc_selrefs 和_objc_methname 的差别可以得出未使用的方法。但是由于 OC 的动态性,得出的结果会有大量误报的情况。那么如何减少误报呢?可以通过测试 case 的代码覆盖率,来提高无用代码精确度的分析。


Xcode 设置中开启代码覆盖率后,编译时会生成 gcno 文件,程序运行时会产生 gcda 文件,通过 lcov 解析即可得到代码测试覆盖率。如果开发阶段开发人员的自测,以及全功能 case 跑完,都有一些类或方法没有被覆盖到,并且这些类或方法又跟 Link Map 文件分析的结果一致,那么这些类或方法会被标记为无用代码,由工具统一收集并指派给相关开发人员进行处理。



针对安装包大小的不断优化非常重要,这也使得爱奇艺 APP 能够在集成文学、奇秀、漫画、泡泡、票务、轻小说、知识付费等各类业务后,安装包大小依然保持在 200MB 以内。

二、启动速度

用户下载 APP 后,第一次打开以及后续使用最先体验到的,便是启动速度,所以启动速度对于用户体验来说至关重要。iOS 上的启动分为冷启和热启,热启由于 APP 已经加载到内存中,通常情况下都是非常快的。一般对于启动速度的优化,主要是针对冷启动而言。


启动可以划分为 before main 和 after main 两大部分,这两部分都存在优化空间。



Xcode 里点击 Edit Scheme,在 Environment Variables 中添加 DYLD_PRINT_STATISTICS 字段,设置为 YES 后可以打印 pre-main time



  • dylib loading time


其中 dylib loading time 包含了加载系统动态库和加载 APP 自己的动态库,系统动态库因为有相关的优化,有些已经加载到内存中了,所以这部分时间很快,也没有优化空间。


加载 APP 内嵌的动态库比较耗时,因为每加载一个动态库,系统都需要文件验证、注册签名、针对 segment 进行 mmap。embedded framework 一般用于 Extension 跟主 APP 共享代码逻辑,苹果针对这部分的优化建议是,如果有多个动态库,合并成一个可以有效的减少加载时间。


为了绕过 iOS8 系统 text 段 60MB 的限制,爱奇艺技术团队之前也使用了 embedded framework,后来针对启动速度优化,又改回使用静态库。使用静态库虽然会增加些许 rebase/binding 的时间,但是综合来看,整体时间还是优于使用动态库的。


  • rebase/binding time & ObjC setup time


iOS4.3 后引入了 ASLR(address space layout randomization),这样 Mach-o 文件和动态库加载到虚拟内存中的地址就不是固定的了,所以需要 rebase/binding 来修正指针。这一步首先会将文件镜像加载到内存并加密,然后针对__DATA segment 中的指针进行修正。


这一步可以优化的操作有:


1、减少 OC 类及方法


2、减少 C++虚函数


针对安装包大小的优化已经针对无用代码进行了处理,所以这一步也间接省略了。


  • initializer time


这一步会按继承关系调用每个类及 category 的 load 方法,一般来说主要耗时也集中在这里。由于 load 被系统自动调用,之前 APP 中很多动态注册的功能都是基于 load 方法,这个机制的滥用导致 load 数量最多超过了 900,也严重影响了启动速度。因而团队选择修改机制,将一些 load 中 逻辑移动到 initialize 中。另一些基于动态注册的类,通过编译器记录下类名并保存到文件,在 APP 启动后适当的时机再读取及调用。


调用机制的修改,让 APP 减少了大概 700 多处 load 方法调用。为了杜绝后续 load 方法的过度使用,爱奇艺技术团队创建了一个工具,该工具 swizzling 系统的 load 方法,并通过动态库的形式加载到 APP 中,这样就可以统计到 APP 中 load 方法的数量、每个 load 方法的耗时以及使用了 load 方法的类所在的库。


各版本数据入库后,也可以方便地跟踪以及分维度查看 load 方法数量及耗时的变化情况。这部分可以重点优化的点是:


1、减少 load 方法的使用


2、如果必须使用 load,精简逻辑,减少代码耗时


  • didFinishLaunchingWithOptions


前面的准备工作都已经完成,这一步主要是生成主视图,所以目标是尽快生成主视图,减少其他不必要的逻辑,部分业务逻辑放到子线程执行,使用代码绘制 UI,减少或者不用 xib 和 storyboard。


此外,用户经常涉及到的启动逻辑应该重点优化。以爱奇艺 APP 为例,据统计,80%以上的用户启动是开机屏广告,所以针对这种启动 case,可以重点优化,比如提前准备好下一次的广告数据,图片下载后直接解压为 bitmap,跟广告展现无关的逻辑延后处理。广告展现的同时,进行首页 UI 的绘制。


这部分逻辑对启动速度影响很大,所以也需要进行监控。针对这个方法,爱奇艺技术团队也创建了对应的工具,工具会记录 didFinishLaunchingWithOptions 方法中每个改动,并对每个方法进行打点计时,这样当 didFinishLaunchingWithOptions 方法中的代码有修改,或者其中的某个方法执行耗时有异常时,可以及时发现并定位。

三、稳定性

APP 稳定性方面主要是减少异常及崩溃,针对这部分,可以从 Category、Method Swizzling 以及静态分析来入手。


  • Category 分析


Category 可以为现有的类添加方法,但是 Category 方法的不规范使用很容易引发问题,其中最容易出现的就是重名问题。重名问题分为两种情况,一种是 APP 内的不同业务 Category 重名,另一种是 APP 内的 Category 方法与系统 API 重名,包括系统私有 API。


首先是不同业务的 Category 方法重名。如果是相同的逻辑,这种情况是方法重复,只保留一份可以减少冗余代码及安装包大小,减少 rebase/binding time。如果是不同的逻辑,那么会导致只有一份逻辑生效,其他业务就会产生逻辑错误甚至导致崩溃。


如果是与系统 API 重名,那么可能影响系统逻辑,尤其是与私有 API 重名时,很难发现问题(因为 document 中搜不到)。之前曾发生过 APP 逻辑异常但一直找不到原因的情况,后来发现是 Category 方法与系统私有 API 重名导致。



比较规范的做法是 Category 名加前缀,但是一些开发人员可能由于各种原因没有这么做。为了及时发现重名情况,爱奇艺技术团队创建了一个工具进行监控,工具会定期将 APP 中所有 Category 方法汇总,分析有无重名情况。与系统 API 的对比需要先将系统 API 提取出来,公开的 API 可以通过解析系统库头文件来提取 API,私有 API 可以通过 class dump 系统库,然后用结果中所有的 API 减去头文件中的提取的 API,就是私有 API。这样就得到一份 iOS 系统的所有 API,用 APP 中的 Category 方法跟系统所有 API 求交集,就可以得到与系统 API 重名的 Category 方法。


  • Method swizzling 分析


Method swizzling 可以解决很多问题,但也会引发一些问题。一般情况下,Method swizzling 主要是为系统方法插入一些逻辑,但有时也会导致修改系统逻辑的情况出现。之前遇到过某些业务修改了系统实现,导致特殊情况下 APP 的崩溃,比如当 NSArray 的元素超过五万时,对象的一些方法会走系统优化后的方法,所以针对这些方法的 swizzling 都会出现问题。


为统计各业务 Method swizzling 的使用情况,团队针对使用了 method_exchangeImplementations 的库进行初筛,针对使用了 swizzling 的库使用 ar -x 后,再针对.o 文件进行二次筛选。最后通过反编译工具,反编译.o 文件,就可以查到使用 swizzling 的具体方法。对于新增了 swizzling 使用的库需要严格审核,确保无问题后才合并到主工程。


  • 静态分析


静态分析可以有效地发现并预防一些问题,如使用 Xcode 自带的 Analyze 以及 Facebook 的 infer 工具。注册观察者未移除、delegate 没使用 weak 修饰、对象未使用以及未做类型或 null 的判断都可以通过静态分析及时发现。



这块要做的主要目的是跟踪分析结果,了解各版本以及各个库新增了多少,修复了多少,掌握其自动创建修复任务及分配情况,确保问题提早发现,数量能够及时收敛。

四、其他维度

  • 私有 API 检测


使用了私有 API 可能导致审核被拒,被拒会影响发版日期,一些推广及功能的上线都会受到影响,因此私有 API 检测也是十分必要的。使用私有 API 被拒的情况主要有:


1、使用了私有 API 或私有类


2、方法名跟私有 API 一样


第一种情况明显要被拒,第二种被拒则比较“冤枉”,比如某一方法跟私有 API 恰好一样,实际并没有使用私有 API,但是仍然被认定使用而遭拒审。第二种情况又分为两个 case,一个是某个类的某个方法跟私有 API 重名了,但是类名并不相同,另一个是继承了系统某个类,恰好一个方法跟这个系统父类的私有 API 重名。第一个 case 被拒的可能性较小,第二个 case 有极大概率被拒。


之前在 Category 分析时,已经获取到了系统私有 API,通过 class dump 以及 strings APP,可以获取到 APP 中所有的 API;通过两者取交集,可以得到 APP 中试用的 API 与私有 API 重名的情况。这些 API 拿到后,需要对其父类以及 Category 类进行查找,如果父类或者 Category 类与系统一致,标记为严重。如果只是重名,标记为中等。


此外,针对线上 APP 的私有 API 分析,可以得出一份白名单(可以上线说明即便命中私有 API,也不会被拒)。当前版本分析出的新增私有 API,如果在白名单中,那么可以暂时不处理,如果不在白名单并且标记严重,一般会让对应业务进行修改,防止提包被拒。



  • 文件读写、解压缩


如果频繁的文件读写与解压缩操作时机跟某些用户操作重合,可能会影响用户体验,比如 APP 启动后,用户可能会进行列表滑动;播放剧集时,快速开播也有特定 IO 逻辑。此时读写文件或解压会导致帧数降低、开播变慢等问题,因此针对这部分的监控也是十分必要的。


通过代码扫描或者 hook 的调用,可以了解哪些库进行了文件读写及解压,将这些操作的时机、当前线程、以及用户行为等信息记录下来并进行归纳分析,就可以知晓这些操作是否当时会影响用户体验了。可以通过用户操作 优先级状态,来决定是否进行这些操作。不过这涉及到多线程操作,需注意死锁的问题。


五、工具箱的架构演进

随着对 APP 的不断优化,工具箱也在不断更新和成长。最初,针对各维度的分析统计都是一个个脚本,数据生成后,对数据的分析和汇总也是人工处理,这个时期可以称之为“石器时代”。



后来会按照分类,将这些比较小的工具归类并组合起来,使之可以针对 APP 的某个维度进行整体分析;工具交给 Jenkins 定时触发,数据的分析和汇总也由人工变成自动分析处理,工具箱进入到“工业时代”。



为了更直观地查看各维度的数据及变化情况,于是为工具箱增加了前端页面,包括数据展示、对比以及一系列的图表系统;当数据异常或者波动较大时,详细数据情况会通过邮件提醒对应的负责人;自动生成的优化任务也会根据规则分配给对应的开发人员,工具箱进入到“自动化时代”。


总结

爱奇艺 APP 目前集成了很多业务,比如奇秀、动漫、阅读、票务、知识付费等,然而安装包大小一直保持在 200MB 以内、保持”秒开”的启动速度、性能方面各页面切换的帧率保持在 55 帧以上,关键页面的滑动保持在 59 帧,同时,开播速度也优于竞品,在稳定性方面的不断完善也使得爱奇艺 APP 的崩溃率小于千分之二,是爱奇艺技术团队的不断优化的结果。为用户提供最好的视频体验及相关服务,爱奇艺技术团队也会在优化之路上继续探索。


本文转载自公众号爱奇艺技术产品团队(ID:iQIYI-TP)


原文链接


https://mp.weixin.qq.com/s?__biz=MzI0MjczMjM2NA==&mid=2247485306&idx=1&sn=93bf70b6168a4f8a56ceab3d2849a844&chksm=e9769b59de01124f8b78a2dc8a189cb6953593db207254f83ace96d76770f951364fa735b204&scene=27#wechat_redirect


2019-08-03 08:002490

评论

发布
暂无评论
发现更多内容

5G边缘计算:开源架起5G MEC生态发展新通路

华为云开发者联盟

开源 5G 边缘计算 公有云 EdgeGallery

LeetCode题解:155. 最小栈,单个栈同时存储最小值,JavaScript,详细注释

Lee Chen

大前端 LeetCode

10万奖金等你拿!2020第四届易观OLAP算法大赛火热开启

易观大数据

向云再出发:如数据般飞驰的内蒙古

脑极体

开发者的福音,LR.NET模块化代码生成器

Learun

Java 敏捷开发 .net core 计算机程序设计艺术 软件设计

controller-manager的主动驱逐

Geek_f24c45

Kubernetes k8s

大事情!中国限制 AI 算法出口。网友:这是要阻止XX“下跪”

程序员生活志

mPaas研发流程和线上运维介绍

阿里云金融线TAM SRE专家服务团队

ios android

Spring整合WebSocket

牛初九

NodeX Component - 滴滴集团 Node.js 生态组件体系

滴滴普惠出行

一键洞察全量SQL ,远离性能异常

华为云开发者联盟

数据库 sql 大数据 数据治理 华为云

Vue+Springboot项目部署

ZRK

Vue 前后端分离 springboot 部署

人民版权 获2020中国产业区块链创新奖

CECBC

区块链 产业发展 版权

Redis常见问题--哈希冲突

是老郭啊

哈希表 Redis项目

【译】Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases 上篇

米乐m6app苹果官网下载

分布式数据库 异步 Amazon Aurora 日志驱动

Spring Boot中获取配置的一些方法

Geek_416be1

Spring Boot 2

新基建迎来风口 新人才仍有缺口

CECBC

人工智能 新基建 数字化基础

JAVA,.NET项目开发难上手?Learun敏捷开发框架解君愁

Philips

Java 敏捷开发 .net core

深入了解 Rust 异步开发模式

lipi

rust 异步

一文带你深扒ClassLoader内核,揭开它的神秘面纱!

我没有三颗心脏

Java ClassLoader java基础 类加载器

Redis常见问题--单线程

是老郭啊

nosql redis 线程

消息队列之事务消息,RocketMQ 和 Kafka 是如何做的?

yes

分布式事务 RocketMQ kafak 事务消息

易观CTO郭炜:如何构建企业级大数据Ad-hoc查询引擎

易观大数据

Redis 持久化--AOF

是老郭啊

redis redis持久化 aof

开发任务管理分析报告

森林

Docker 镜像的备份恢复迁移

哈喽沃德先生

Docker 容器 微服务 镜像

JVM 内存模型、字节码、垃圾回收面试要点

escray

学习 面试 垃圾回收 字节码

一个空格引发的“救火之旅” - 记一次 SOFA RPC 的排查过程

阿里云金融线TAM SRE专家服务团队

数字化转型需要低/零代码平台的支持

代码制造者

低代码 数字化转型 企业信息化 零代码 编程开发

银行大数据新玩法,构建“一湖两库”金融数据湖

华为云开发者联盟

大数据 数据湖 FusionInsight MRS DWS

OpenKruise:Kubernetes 核心控制器 Plus

郭旭东

Kubernetes 云原生 OpenKruise

爱奇艺移动应用优化之路_移动_Razor_InfoQ精选文章