大小实践SouliOS防劣化(文件分析大小资源增量)

1 引言:为什么要关注包大小包体治理是每一个App都需要关注的优化项目,针对iOS而言,200MB的App包体下载大小更是一个关键的临界指标。
因为超过200MB的App,进行下载时苹果会提示是否使用Wifi下载,会对新客的转化率造成较为严重的影响。
包体的治理势在必行。
图1:包大小分为下载大小(Download)和安装大小(Install)两个概念,苹果的200MB限制是基于下载大小的,我们一般关注的也是下载大小。
在不同的设备上,iOS包体的表现不尽相同,我们以设备中下载大小最大的数值作为参照指标(我们一般看iPhoneX)图2:超过200MB的App在运营商网络下载时提示的「Wifi下载提示弹框图3:苹果手机的流量下载弹框的配置位置:Settings→App Store→App Downloads。
默认配置是「Ask If Over 200MB」2 包体治理的思路包体的治理可以简单分为「优化」和「防劣化」2.1 优化 - 造减量常见的优化方案如:工程配置层面:编译等级配置/StripCode配置/TEXT段转移等(针对TEXT段转移,小伙伴们可能比较陌生,可以参考下这篇文章:http://www.kffy.cn/meiwen/359417.html,本篇不详情展开)未使用文件清理:未使用资源清理/未使用源文件清理等大资源优化:压缩/远程化等2.2 防劣化 - 防增量随着业务的持续迭代,我们的的包体的自然行为几乎必然是持续变大的,他们主要包含:新增的业务代码增量新增的三方库增量新增的直接放置到包体的大资源废弃的未移除的旧代码/旧资源我们当前基本防劣化思路是:约定软性的增量准入规则进行版本包体变化监控,跟进处理逾越准入规则的增量项目不定期进行包体优化Soul包体防劣化思路说明图基于我们的防劣化思路,我们可以发现一些基本的需求问题点:如何确定产生包体增量的模块与相对准确的增量值?如何发现版本新引入的大资源?如何快速定位相关资源/代码引入的关键人?如何发现废弃的资源/源文件?纯靠人力处理上述问题显然不是有效持久的方案,我们需要工具的支持。
2.3 工具辅助从高ROI的角度出发,我们优先考虑第三方工具。
我们发现了LSUnusedResources(https://github.com/tinymind/LSUnusedResources),它无疑是一款协助发现无用资源的优秀的工具,但针对我们的需求,它存在明显的不足:只能整体发现无用资源(无法区分模块,无法分析有效资源,无法分析源文件,无法对比版本等)它是一个桌面工具,难以基于脚本直接调用使用的文件发现方式较慢(使用 /urs/bin/find 命令)同时,考虑到需要:结合我们App本身的工程结构特性分析应对紧急的特例分析需求变化等因素,我们决定自研一套App包体的分支支持工具。
3 Soul的包体分析讲到包体分析,我们第一想到的未使用资源(图片)的清理,它需要基于工程源码进行分析。
但在我们基于源码分析进行工具开发的过程中发现,它很难准确分析模块迭代的二进制增量,因此我们进一步引入了LinkMap分析,形成了 「LinkMap分析二进制 & 源码分析资源」的基本模式,每种模式都包含「增量监控」和「存量分析」。
Soul包体分析的功能支持上图即为Soul包体分析的功能支持示意,下面我们详细看看LinkMap分析和源码分析的一些关键技术点。
3.1 LinkMap - 分析二进制什么是LinkMap?编译一般包括「预处理-->编译-->汇编-->链接」几个阶段。
Link Map File 即 链接映射文件,是 Xcode 生成可执行文件时一起生成的文件,用于记录链接相关信息。
iOS工程在Xcode中配置了Target-Build Setting-Linking-Write Link Map File 为 YES,即可让工程编译后自动生成LinkMap文件生成的LinkMap一般在DrivedData目录下面。
LinkMap一般包括Path,Arch,Object files,Section,Symbols,Dead Stripped Symbols几个部分。
3.1.1 为什么要基于LinkMap分析基于源文件的大小变化来判断包体二进制增量,我们发现它的局限性很大:源文件的增量&包体的二进制增量很难建立起有效的映射关系;随着二进制组件的增多,源文件对包体增量归因愈发不准确;因为归因准确性不强,问题的反馈&跟进阻碍重重我们亟需一种方式,能更真实准确地反应包体二进制的真实变化分布。
经过调研发现,基于LinkMap的分析可以获取到每个模块的二进制大小,进而比对增量,符合我们需求。
包体各模块二进制增量呈现如图,基于新旧2个工程LinkMap的对比分析,可获取各模块的二进制增量变化排序。
3.1.2 LinkMap的关键分析流程说明1)获取.o文件编号LinkMap文件 Object files段示意.o 文件可以简单理解为一个源文件(比如aaa.m)的二进制形式(即aaa.o)。
如图,在LinkMap文件的「Object files」部分,我们可以获取到所有链接的 .o 文件的两项关键信息:编号:左边的[0][1][2][3]....o文件路径:编号右边的内容我们先关注下「文件编号」2)聚合.o文件符号,获取.o文件大小LinkMap文件 - Symbols段示意接下来我们关注下LinkMap文件的Symbols部分,我们可以看到两个关键的讯息:Size:特定符号的大小File: 特定符号所在的文件编号将相同文件编号的符号聚合起来,我们就可以获取到对应.o文件的大小。
3)聚合.o文件,获取模块大小LinkMap - Object files段包含模块信息在LinkMap文件的「Object files」部分,.o文件的路径信息其实已经包含了模块相关信息,参考它,我们可以区分以pod为基础的模块。
将相同模块的.o文件进行聚合,就可以获取模块的大小。
4)基于模块比较两份LinkMap文件的分析结果两份工程LinkMap的对比结果将新旧2份工程的LinkMap文件基于模块的二进制大小聚合结果进行对比后,输出成csv表格形式(方便利用表格的筛选/排序功能),可快速关注到App版本迭代的二进制增量变化。
3.1.3 LinkMap分析的工具架构了解了LinkMap分析的关键技术流程,再关注一下我们LinkMap分析工具的架构。
Soul LinkMap分析工程架构1)main提供命令行参数支持触发LinkMap解析&比较流程2)Compare2.1)CYLinkMapComparer支持两个完成分析的CYLinkMap对象的比较记录比较结果进行必要比较结果抽取2.2)CYLinkMapCompareItem描述两个项目的大小比较结果属性含:比较备注,大小改变,状态(修改/新增/删除/无变化)比较结果承载模型3)LinkMap Digger3.1)CYLinkMap支持LinkMap文件的解析记录LinkMap解析原始数据记录LinkMap即系聚合数据支持LinkMap分析结果写文件提供整体从解析,到写文件的流程支持3.2)CYModule用以LinkMap中object对象的模块聚合。
直接的object对象过于零碎,我们更多关注的是模块的变化,比如一个pod的变化3.3)CYObjectFile描述LinkMap文件中 Object files 部分的单项的数据结构负责 Object 单行信息的解析记录其关联的符号信息3.4)CYSymbolItem描述LinkMap文件中 Symbols / Dead Stripped Symbols 两个部分的单项的数据结构负责 Symbol 单行信息的解析主要关注其 size 和 fileId,前者用于计算大小,后者用户关联object对象3.5)CYSectionItem描述LinkMap文件中 Sections 部分的单项的数据结构负责 Section 单行信息的解析4)Utils4.1)CYFileWritter提供文件输出的相关操作,封装定制输出csv文件接口4.2)LineIterator支持文件的流式逐行读取3.2 工程源码 - 分析资源LinkMap分析可以有效的分析二进制的变化,但对于随应用打包的资源(图片/音频/视频)等却无能为力。
实践过程中我们发现,造成包体异常增量的关键因素往往正是大资源的随意引入。
我们需要基于资源的 增量/存量 信息来指导治理,获取这些信息的方式可以通过分析工程源码实现。
3.2 工程源码 - 分析资源3.2.1 技术点 - 正则提取在「未使用资源查找」的技术需求中,最基础的一个操作即为判断某个资源是否被某个文件所「使用」,它往往需要在一个文件中提取到符合某个特定范式的字符串,这就需要用到正则(关于正则的规则约定,在此不再赘述,网上可以找到很多完备的资料)。
从源文件中提取目标图片名如图的defaultPic文本,可以通过正则「@\"(.?)\"」快速提取。
3.2.2 技术点 - 基于TagString的效率优化基于「未使用资源查找」来简单介绍下TagString的查找算法思路与优势。
场景举例:一份工程中有 5000个png图片 和 20000份.m源文件1)多重循环的思路即针对每个图片,依次判断它是否在20000个源文件中被匹配,若均没有,则认为该图片为未使用的图片。
伪码如下:noUsedImageArray = [] // 未使用图片数组for (image in allImages) { // 共5000张图片 imageUsed = false // 标记某个资源是否被使用 for (file in allFiles) { // 共20000份源文件 if (file中包含image的名称) { imageUsed = true break } } if (imageUsed = false) { noUsedImageArray.append(image) }}return noUsedImageArray2)TagString的思路先提取20000个源文件中的所有可能为图片名的关键字符串(TagString)并存入一个随机查找的数据结构中(如Set),然后再看5000个图片名是否在这个TagString的Set中存在。
文件中的tagString提取示意伪码如下:

大小实践SouliOS防劣化(文件分析大小资源增量)

tagStringSet = Set()for (file in allFiles) { // 共20000份源文件 tagStrings = extractTagStringFromFile(file) tagStringSet.addObjects(tagStrings)} noUsedImageArray = [] // 未使用图片数组for (image in allImages) { // 共5000张图片 if (image not in tagStringSet) { noUsedImageArray.append(image) }}return noUsedImageArray两种无用资源提取思路的比较:常规双循环-运算量:1亿次 (5000 20000)tagString-运算量:2.5万次 (5000 + 20000)可以看出来,使用TagString的方式,运算速度的提升有接近4个数量级TagString的提取正则下面是一些常见文件中提取图片资源TagString的正则参考,对于不同类型的文件,其使用图片的范式是有差异的。
self.conf_regexDic = @{ @"h": @[@"([a-zA-Z0-9_-])\\.(png|jpg|imageset|svg|json|mp3|gif|caf)"], @"m": @[@"@\"(.?)\""], @"mm": @[@"@\"(.?)\""], @"swift": @[@"\"(.?)\""], @"xib": @[@"image name=\"(.+?)\""], @"storyboard": @[@"image name=\"(.+?)\""], @"strings": @[@"=\\s\"(.)\"\\s;"], @"c": @[@"([a-zA-Z0-9_-])\\.(png|jpg|imageset|svg|json|mp3|gif|caf)"], @"cpp": @[@"([a-zA-Z0-9_-])\\.(png|jpg|imageset|svg|json|mp3|gif|caf)"], @"html": @[@"img\\s+src=[\"'](.?)[\"']"], @"js": @[@"[\"']src[\"'],\\s+[\"'](.?)[\"']"], @"json": @[@":\\s\"(.?)\""], @"plist": @[@">(.?)<"], @"css": @[@"([a-zA-Z0-9_-])\\.(png|jpg|imageset|svg|json|mp3|gif|caf)"],};3.2.3 技术类比 - 无用源文件的发现思路tagString仅仅可以用于无用资源检索么?不,这套模式还可以用于其他的分析,比如:基于头文件判定的无用源码文件发现(提取头文件引用内容作为tagString进而判断未使用的文件,当然,对swift无效……)。
下面是一些常见文件中提取import文件名TagString的正则参考:// 无用源码文件发现-提取头信息self.conf_regexDic = @{ @"h": @[ @"#import[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#import[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#import./([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#include./([a-zA-Z0-9_\\-\\+])\\.h", @"@import[ ]([a-zA-Z0-9_\\-\\+])", ], @"m": @[ @"#import[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#import[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#import./([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#include./([a-zA-Z0-9_\\-\\+])\\.h", @"@import[ ]([a-zA-Z0-9_\\-\\+])", ], @"mm": @[ @"#import[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#import[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#import./([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#include./([a-zA-Z0-9_\\-\\+])\\.h", @"@import[ ]([a-zA-Z0-9_\\-\\+])", ], @"c": @[ @"#import[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#import[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#import./([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#include./([a-zA-Z0-9_\\-\\+])\\.h", @"@import[ ]([a-zA-Z0-9_\\-\\+])", ], @"cpp": @[ @"#import[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#import[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#import./([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]<([a-zA-Z0-9_\\-\\+])\\.h", @"#include[ ]\"([a-zA-Z0-9_\\-\\+])\\.h", @"#include./([a-zA-Z0-9_\\-\\+])\\.h", @"@import[ ]([a-zA-Z0-9_\\-\\+])", ], @"swift": @[ @"import[ ]([a-zA-Z0-9_\\-\\+])", ]};3.2.4 增量监控通常,针对迭代产生的增量数据(某个模块的二进制新增了300KB,新增了某个80KB的资源等)关注度是更高的,分析控制增量是控制准入的不二法门。
1)定义文件数据结构程序=数据结构+算法,良好的数据结构是简单有效的程序的基础,增量监控工具一样需要一些关键数据结构的支持,比如一个文件包含很多的基础信息:文件目录,文件名,文件大小等。
针对分析中,我们常见用的数据信息,提前在model中进行抽取,以对后续分析的便捷性帮助。
如下是我们简单定义的一个FileItem数据结构的部分属性。
/ 基础文件信息 /@interface CYFileItem : NSObject @property (nonatomic, strong) NSString filePath; / 文件路径 /@property (nonatomic, strong) NSString fileReletivePath; / 文件相对路径 /@property (nonatomic, assign) uint64_t fileSize; / 文件大小 / @property (nonatomic, strong) NSString fileName; / 文件名称(含后缀) /@property (nonatomic, strong) NSString pureFileName; / 文件名称(不含后缀)/@property (nonatomic, strong) NSString fileSuffix; / 文件后缀 /@property (nonatomic, strong) NSString fileTag; / 文件标签,用以匹配 / @property (nonatomic, assign) CGSize imageSize; / 当文件是图片类型文件时,的size大小信息 / @end2)分析资源的迭代变化在基础数据结构的基础上,如果想关注增量资源变化,分析新旧2个工程的资源差异就可以达成。
根据「文件相对路径是否有变化」和「文件大小是否有变化」,我们将资源的变化约定为6种类型:无变化,新增,删除,修改,无变化转移,修改并转移。
我们一般着重关注的是新增大资源,迭代变化的分析结果参考如下:工程对比发现的迭代新增资源3.2.5 工程源码分析的架构简述1)上层分析器DiggerBase抽象了基础的配置属性(输入输出目录等)和基础的流程方法(基于正则/TagString的分析封装等)在DiggerBase的基础上,可以快速定制需要的分析器。
是一个简单的工厂模式2)中层流程工具文件查找:遍历目录文件并转化为基础的FileItem结构分组构造:获取自动分组(比如基于Pod的分组) & 定制分组(比如基于特定目录的分组)内容抽取:正则TagString提取文件分组判定:为文件&分组添加关联信息输出:提供方法封装快速输出csv文件3)下层基础数据模型FileItem:文件数据结构GroupItem:分组数据结构FileAggreInfo:聚合分析时,我们常常需要总值/均值之类的聚合信息,通过该结构承载CompareItem:承载版本比较结果的一个数据结构,它通常包含多个FileAggreInfo,比如:新增项目聚合信息,删除项目聚合信息等。
4 插上自动化的翅膀有了一些基础的工具后,确实让一些事情「可以进行」,但我们难免还是会感到繁琐。
比如每次进行LinkMap分析时,我们需要:checkout 2份工程(用以对比)将每份App工程切为全源码对每份工程pod install进行每份工程的编译找到每份工程的LinkMap文件位置运行分析工具分析结果而这些步骤中,只有「分析结果」是需要掺杂一些经验和思考的。
前置的其他步骤,零碎繁琐易出错不说,也很耗时(至少需要 40min),极其影响效率,这部分工作可以通过自动化来实现。
我们以Jenkins为基础,配置了如下流程,用以简化繁琐耗时的重复性工作。
LinkMap分析的自动化流程示意这样,每次我们需要进行分析时,只要点击对应链接下载分析结果就好了。
在我们包体日常分析的过程中,针对发现的一些包体问题(比如大资源),是需要处理解决的;而解决具体的问题,一定要有跟进人;最合适的跟进人无非是资源的添加或调整人。
大资源问题推进思路示意我们可以通过git log的方式获取到资源的调整人信息。
分析输出结果补充调整人信息的示意操作git log 这个环节,我们也是可以让工具协助的。
为了保障工具的执行效率,我们只针对关键的资源进行资源调整人信息的补充,它们包括:Top的大资源迭代新增/变化的资源GitLog的在工具中使用的的操作命令,和关键信息提取的方法,这边简单分享一下:gitlog命令操作示意提取特定资源X的git log前3行命令示意:git -C [资源X所在目录] log [资源X路径] | head -n 3提交号正则:"^commit(.)"操作人正则:"^Author:(.)<"邮箱正则:"<(.)>"日期正则:"^Date:[ ](.)"5 「斜杠」工具包体分析工具并非仅应用在包体优化上面,对工具的基础功能进行简单的扩展后,它能为App其他层面的性能治理提供有益的帮助。
5.1 崩溃治理辅助- 文件模块在进行Crashe分析的时候,我们常常遇到一些符号可能在二进制文件/三方库中,难以快速确认其代码位置/模块归属。
此时,使用LinkMap分析衍生输出的「对象名称 - 所属模块」映射关系表,往往能快速定位问题模块。
对象-模块 映射关系表示意5.2 内存治理辅助 - 宽高大图发现图片在内存中的占用是和图片的宽高乘积强相关的(而非图片文件大小)。
基于包体分析工具的基础文件数据结构,在其中补充图片宽高的属性字段,并在分析图片文件时进行填充,就可以对比发现宽高大图并将相关信息有效输出。
宽高大图发现示意5.3 内存治理辅助 - 单例发现单例内存一般是不会释放的,一部分内存问题的发生是由于单例的不合理持有造成的。
那么,基于单例的定义特征设计一批正则,就可以提取出出工程中存在的单例类,以辅助单例治理的分析。
单例发现示意以上为该次分享的内容,感谢您的阅读。
作者:Soul-iOS 来源-微信公众号:Soul技术团队出处:https://mp.weixin.qq.com/s/m5ncSZEXozn8kU0e6YaLTQ

联系我们

在线咨询:点击这里给我发消息