1背景

跟着移动App不断的开展, 从以往粗放式叠加需求的办法,现已转换为怎么运用技能手段去管理APP的各项方针的新办法。包巨细也是衡量APP的一项目重要方针,其直接影响着APP下载转化率。本文经过58同城包巨细管理的实践经验,来讲解怎么处理混编环境下OC/Swift无用类、无用资源、重复资源等检测问题,同时结合业界常见的段搬迁、链接时优化(LTO)等多种技能手段,来辅佐App进行减肥。

1.1 包巨细管理的痛点

  • 包巨细管理触及无用代码、无用资源的检测、优化等诸多东西,这些东西分散难以开箱即用,运用成本高;

  • 市面上开源东西对OC/Swift混编架构支撑缺乏,特别是无用代码检测在混编架构下不精准;

  • 要全面管理APP中代码文件,需求对Mach-O有比较深化的了解,而Mach-O的学习成本较高;

1.2 技能特点

本东西只需求拖入Debug下的构建产物(.app),就能一键获取包减肥各种优化主张。本东西经过读取Mach-O可履行文件中__Text、__DATA、符号表中等数据,来获取Swift类型结构、类名字符串、函数调用区间等辅佐信息, 并经过反汇编提取__TEXT/__text中的指令,来履行一些重要的查询操作,如Swift中AccessFunction地址的查找。目前本东西支撑以下检测项:

检测类型 是否支撑 备注阐明
混编Swift下OC类检测
58同城iOS包大小治理工具解密
混编OC下Swift类检测
58同城iOS包大小治理工具解密
Swift无用类检测
58同城iOS包大小治理工具解密
RunTime无用类检测
58同城iOS包大小治理工具解密
OC无用类检测
58同城iOS包大小治理工具解密
无用资源剖析
58同城iOS包大小治理工具解密
重复资源剖析
58同城iOS包大小治理工具解密
段搬迁运用办法
58同城iOS包大小治理工具解密

2 关键技能完成

2.1 包巨细检测

.app包中首要包括二进制文件、Assets.car资源文件、nib等其它类型的资源文件等组成部分。

二进制文件包括主二进制、PlugIns中的二进制,还有Frameworks中的动态库文件,占整个APP大部分体积。对这些二进制文件,剥离架构后核算单架构文件巨细即是二进制巨细。二进制检测的项目首要有OC/Swift无用类、段搬迁、动态库的符号表剥离,也是首要的优化方向,除此以外,还有链接时优化(LTO)。

资源文件中首要检测Assets.car、.car外的图片、其它资源文件。关于Assets.car文件需求分片检测其巨细,咱们首要核算3x下的巨细。关于.car外的图片,假如有1x/2x/3x图片存在,将其放入.xcassets中管理也有必定的优化空间。关于其它资源,咱们首要检测在APP中是否运用,以及哪些图片是重复的。

一切的检测成果咱们在确诊视图上分项列出,并供给链接文件的办法保存到指定目录,研制人员能够检查更具体的成果数据。

2.2 无用类检测

在APP经过长期事务迭代后,或多或少都会有一些事务下线,为了使APP能持续减肥状况,就需求有事务检测才能,而无用类检测一般是最直接的办法。现在APP首要分为纯OC环境、纯Swift环境和OC/Swift混编环境,而且OC/Swift混编环境占多数。

关于纯OC环境,因为OC言语现已很成熟,结构相对较简略,无用类检测计划也相对简略些。而关于纯Swift或OC/Swift混编环境的无用类检测,因为Swift言语自身稳定的时刻并不长,而且其结构相对OC来说杂乱许多,OC中的检测计划在Swift中并不适用。因而,咱们需求寻求一种能适用于OC/Swift混编环境的无用类检测计划。

2.2.1 OC无用类检测

关于OC无用类的检测,业界比较盛行的计划是classlist段和classrefs做差集,其间classlist是一切OC和Swift类的全集,而classrefs是引证类的调集,如下图所示。这种计划能粗略地核算出OC无用类,但也存在一些缺乏。一是动态调用类识别不出来;二是外部没有调用,可是+load中需求履行一些操作,如hook办法,这些类识别不出来。因而,需求对classrefs调集做进一步扩展。

58同城iOS包大小治理工具解密
  • 检测动态调用

动态调用一般有两种,一是完整的类名字符串生成class调用,二是经过拼接类名或许经过后台下发类名来生成class调用。关于后台过于杂乱,不在咱们的评论范围内,这儿首要评论前者怎么检测。关于经过类名生成的class,一般OC中的字符串会记录到__DATA,__cfstring段中,因而,经过遍历__cfstring中的字符串,能找到一切动态调用的类名,并将这些字符串添加到classrefs调集中。

  • 检测没有调用联系但完成了load办法的类

而关于外部没有直接的调用联系,可是完成了+load办法的类或类别,一般也是需求用的类。遍历__DATA,__objc_nlclslist段中的类,这些类是完成了+load的类,将其添加到classrefs调集中,再遍历__DATA,__objc_nlcatlist段中的类别,这些类别中完成了+load办法,将其添加到classrefs调集中。

  • 扩展后再做差集

classrefs经过以上扩展后,再经过classlist和classrefs做差集,得到的OC无用类检测成果更为准确。

2.2.2 混编环境下Swift无用类检测

比较纯OC项目, 换边状况下,OC调用Swift类改怎么检测呢?这要从Swift Class 界说说起,让咱们看在runtime源码中Swift Class的界说办法:

struct swift_class_t : objc_class {
    uint32_t flags;
    uint32_t instanceAddressOffset;
    uint32_t instanceSize;
    uint16_t instanceAlignMask;
    uint16_t reserved;
    uint32_t classSize;
    uint32_t classAddressOffset;
    void *description;
    // ...
    void *baseAddress() {
        return (void *)((uint8_t *)this - classAddressOffset);
    }
};

经过上面发现, swift_class_t是继承自OC类模型swift_class_t的, 所以混编环境中的Swift Class的特性与OC中Class特性共同, 直接用2.2.1中的检测计划即可。

2.2.3 Swift无用类检测

比较OC,Swift无用类的检测就更杂乱,首要是Swift的类结构,如下所示,与OC差得比较大,再一个是classrefs段中并没有保存Swift的引证类。经调试会发现,外部在调用Swift类中函数之前,会先调用该类的AccessFunction函数获取该类的MetadataClass地址,也便是说,外部拜访Swift类的办法时,首要需求拜访AccessFunction地址。

struct SwiftClassType {
    uint32_t Flag;
    uint32_t Parent;
    int32_t  Name;
    int32_t  AccessFunction;
    int32_t  FieldDescriptor;
    int32_t  SuperclassType;
    uint32_t MetadataNegativeSizeInWords;
    uint32_t MetadataPositiveSizeInWords;
    uint32_t NumImmediateMembers;
    uint32_t NumFields;
    uint32_t FieldOffsetVectorOffset;
    uint32_t Offset;
    uint32_t NumMethods;
    // VTableList等变长字段
};

经过这一思路,咱们能够经过遍历APP中各类的办法完成中的汇编指令,遍历bl跳转指令,假如后边的回来地址为某个Swift类的AccessFunction地址,则该Swift类标识为有用类,并添加到classrefs调集中。为了对APP中一切自界说Swift类的检测,咱们需求借助符号表,因而,关于Swift无用类的检测,需求用到debug下的.app包。整体完成计划是先要遍历出一切Swift类的AccessFunction调集,再遍历符号表,对每个函数的开端地址进行排序,定位出每个函数的开端和完毕地址,然后经过对函数完成进行反汇编得到函数的汇编指令,终究经过每个函数的开端和完毕方位,结合函数的汇编指令,查找函数范围内的bl指令地址,和前面AccessFunction调集进行匹配,能匹配上的而且当时函数不是AccessFunction所在的Swift类,即可将该Swift类加入到classrefs调集中。

  • 读取一切Swift类的AccessFunction调集

要读取一切Swift类的AccessFunction调集,需求从__TEXT,__swift5_types进行遍历,如下所示。

58同城iOS包大小治理工具解密

swift5_types中存储的是SwiftClassType结构的偏移地址,其加上__swift5_types的开端地址后,是指向__TEXT,__const段,____const段存储的是SwiftClassType结构体数据。遍历__const段中SwiftClassType结构体数据可得到AccessFunction调集,具体根据需求能够存储为<Swift类名,AccessFunction地址>的key-value类型的结构,具体完成参阅WBBlades的开源代码。

  • 定位函数的开端和完毕地址

有一个条件,这儿要检测的Mach-O文件必须是带有符号表的。首要咱们要读取Symbol Table段符号表数据,把其间每个函数的符号和开端地址解析出来,如下图所示。

58同城iOS包大小治理工具解密

其存储的数据结构如下,经过Load Commands/LC_SYMTAB中string table offset的偏移+nlist.n_un.n_strx地址即可在字符串表中读取到当时函数的symbol字符串,而nlist.n_value即为当时符号的函数开端地址begin,将symbol字符串和开端地址保存到数据结构<symbol字符串,begin>中。

struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

再对begin地址进行从小到大排序,将下一个符号的begin地址记为当时符号的完毕地址end。

  • 函数完成中查找AccessFunction地址

关于每个函数定位了开端和完毕方位后,咱们就能够在函数汇编指令中进行AccessFunction的查找了。为了便于操作,首要咱们需求对二进制进行反汇编得到函完成的汇编指令,这儿咱们引入了三方开源代码Capstone。

(1)计划一

开端咱们的查找计划是经过对Swift类进行遍历,对每个Swift的AccessFunction地址,去遍历一遍符号表,经过符号的开端和完毕指令,对当时函数指令范围再进行遍历,判断其间bl地址是否射中前面的AccessFunction地址,假如射中,则阐明AccessFunction地址对应的Swift类是有用的。可是经过58同城Mach-O二进制文件进行测试,出现了功用问题,内存峰值太高,达到6G+,而且遍历时刻过长,整个检测进程经历了1.5小时,如下图所示。

58同城iOS包大小治理工具解密

检查同城APP中汇编指令数4200W+条,符号表中符号数达400W+,Swift类2000+个,整个AccessFunction地址查找进程时刻杂乱度为10^7×10^6×10^3=10^16。其间10^7为汇编指令的量级,10^6为符号表量级,10^3为Swift类量级,这个数量级太大了,故履行时刻很漫长,这个履行时刻关于用户来说,是不行接受的。因而,有必要对汇编指令及AccessFunction地址的战略做些调整。

(2)计划二

经过剖析,汇编指令读取这块能够在量上做一个优化,而且AccessFunction地址查找办法也能够做一个优化。汇编指令读取优化战略为只读取bl指令,保存到汇编指令数组中,因为咱们要剖析的只要bl指令,这个数量级为600W+。AccessFunction地址查找办法调整为遍历符号表,结合bl指令数组找出当时函数范围内的bl指令,再经过当时bl指令后的地址去AccessFunction列表中反查Swift类名,假如查到即射中了某个Swift类,则阐明此Swift类是有用的。

按调整后的计划,咱们将指令和其对应的指令方位保存到指令数组中,如下图所示。指令方位是指在__text段中出现的次序下标,从0开端,因为每条指令占用4个字节巨细,因而,经过当时指令地址-__TEXT__text的开端地址可算出当时指令的偏移地址,再经过偏移地址/4便是指令对应的下标。同理在符号表遍历成果中begin和end也能找到其在汇编指令中的一个方位,这个方位或许是穿插在两个bl方位中心。

58同城iOS包大小治理工具解密

对前面遍历符号表的成果列表进行遍历,经过begin和end能够在指令数组中找到一个开端和完毕的方位区间,如[44,123],然后在这个区间内遍历一切的bl指令,和前面的AccessFunction调集进行匹配,并将匹配上的Swift类加入到classrefs调集中。

58同城iOS包大小治理工具解密

调整后时刻杂乱度为10^6×10^6=10^12,其间第一个10^6为汇编指令的量级,第二个10^6为符号表量级,比较初始计划下降了4个数量级。内存峰值1.7G,AccessFunction地址查找时刻削减到36秒,内存和查寻时刻都优化到了可接受的范围内,如下图所示。

58同城iOS包大小治理工具解密

(3)计划三

履行时刻是否还能够优化点?细细剖析,指令数组还有必定的优化空间,能够对bl指令进一步过滤,指令数组中仅保存与AccessFunction地址相匹配的bl指令。经剖析,这些指令数量级为8000+,时刻杂乱度为10^3×10^6=10^9,其间10^3为指令数组的量级,10^6为符号表量级,内存峰值为1G,整个查找时刻为20秒。比较前一计划,时刻杂乱度下降了3个数量级,内存和查找时刻优化近一半。

58同城iOS包大小治理工具解密

(4)计划总结

总结以上计划数据如下:

计划 时刻杂乱度 内存峰值 履行时刻
计划一 10^16 6G 1.5小时
计划二 10^12 1.7G 36秒
计划三 10^9 1G 20秒

至此,Swift有用类在classrefs调集中也完成了扩展。终究,经过classlist段和classrefs做差集,得出一切无用类,其间也包括了OC的无用类,具体代码完成请参阅WBBlades。

2.3 无用资源检测

了解包巨细的构成,除了对二进制内容进行剖析以外,包内的资源也是需求进行监控的。基于对各类二进制内搜集到的字符串与包内资源文件名的匹配检测,找出包内或许存在的无用资源文件。

二进制信息提取

为了检测包内无用资源,咱们首要要做的是从二进制中提取出一切的字符串信息。

除主Mach-O文件以外,包内资源还有或许被nib、动态库运用。为了检测成果更加准确。需求将包内一切的Mach-O文件、nib文件都归入检测范围内。

咱们能够运用WBBlades东西直接对Mach-O文件字符串进行提取。而nib文件因为被特别紧缩过,所以无法直接提取。可是经过强行打开发现其间的资源文件名等字符串信息没有进行紧缩,所以咱们能够直接将nib文件转为字符串然后再进行分割将其提取出来。

完成字符串信息提取后,接下来咱们要做的便是要对文件名进行遍历提取。

文件名提取

一般状况下的普通文件名的提取是最简略的,咱们能够直接经过遍历bundle内一切文件获取。可是实际操作时仍是会有许多特别问题需求处理。

Asset包内文件名

首要是Asset包内文件的问题。App的包内只能够获取到Asset内文件编译后的.car紧缩文件,其间的内容无法提取。所以咱们必须先对Asset文件进行解压。能够运用开源东西 cartool对.car文件内容读取获得到Asset包内文件名。

图片文件名

与一般文件的引证时运用的文件名不同,图片资源在加载时有或许会有多种状况。比方一张名为“cover.png”的图片,咱们既能够运用”cover”来获取图片资源,也能够运用”cover.png”来获取图片资源。

这就需求为每个文件都进行别号匹配。比方图片”cover@2x.png”,需求增加别号”cover”、”cover.png”、”cover@2x.png”,

Bundle包内文件名

别的假如一个资源文件在bundle中,咱们有时会以”xxx.bundle/xxx.json”的办法来直接获取运用,所以在获取文件名时,也要将文件所处的bundle信息找出,以弥补在bundle内的资源文件或许运用的文件名。

模糊文件名

上述的文件名获取办法都是经过总结常用文件名引证规则而主动生成的。可是开发进程中也常常会运用一些自定的文件名规则来读取资源数据。

比方:界说了一组图标文件名为“icon1.png”~“icon10.png”,结合事务逻辑会动态运用“icon%@”、“icon%@.png”这种规则来读取资源数据。

为处理该问题,需求支撑自界说别号的功用。咱们能够将icon1~10装备到一个plist装备文件内,然后为这些装备自界说别号”icon%@”、”icon%@.png”,这样在无用资源检测的时分,就会经过装备文件获取到自界说的文件名,使终究无用资源文件的检测成果更加准确。

重复资源处理

除了对未运用的资源文件,内容相同的资源文件也算是一种无用资源。尤其是在跨团队配合的项目中,常常因为沟通不畅导致引入了不同名称但内容完全相同的重复资源文件。所以咱们会将一切文件进行一次SHA值核算,然后将相同SHA值的文件检测出来,作为无用资源文件检测成果的弥补。

2.4 其它检测

2.4.1 段搬迁检测

iOS 13 以下的用户,若APP的下载巨细超过200M限制,将无法运用蜂窝网络下载 App,会收到文件容量太大的提示,需经过 Wi-Fi 网络下载。iOS 13 及以上的用户,需求手动设置才能够运用蜂窝网络下载 App。

APP包上传到AppStore后,苹果会对可履行文件进行加密,再发布到AppStore,这种加密会严重影响可履行文件的紧缩效率,导致紧缩后的.ipa 巨细增加,从而下载巨细增大。因为苹果只会对二进制Mach-O 文件中的__TEXT段加密,因而,理论上只要把__TEXT段中的section移到其它段,如自界说的一些segment,就能削减苹果的加密范围,使紧缩效率提升,终究能减小APP的下载巨细。

因为__TEXT,__text段巨细是最大的,因而,假如段搬迁__text段搬迁的收益是最大的,假如__text段不搬迁的话,仅其它段搬迁包巨细收益会大打折扣。因而,咱们能够检测__text段是否在__TEXT中来判断是否有段搬迁。

58同城iOS包大小治理工具解密

如上图,APP存在段搬迁,其间__TEXT中现已没有__text段,而是移到了自界说segment __WB_TEXT。如需求段搬迁,能够在Xcode->Build Settings->Other Linker Flags中进行如下装备。其间__WB_TEXT为自界说segment名,将__text和__stubs搬迁到此segment下,并将__TEXT中只读的section如__objc_methname和__objc_classname搬迁到只读Segment __RODATA中。

-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_section,__TEXT,__const,__RODATA,__const
-Wl,-rename_section,__TEXT,__text,__WB_TEXT,__text
-Wl,-rename_section,__TEXT,__stubs,__WB_TEXT,__stubs
-Wl,-segprot,__WB_TEXT,rx,rx

2.4.2 LTO

Link-Time Optimization 链接时优化(LTO),是苹果在WWDC 2016提出,在Xcode 自带的一个编译/链接参数。LTO是链接期间的程序优化,多个中心文件经过链接器合并在一起,并将它们组合为一个程序,缩减代码体积,因而链接时优化是对整个程序的剖析和跨模块的优化。开启LTO后首要是能给包巨细带来优化。

LTO优化办法如下:

  • 链接器首要按照次序读取一切方针文件(此时,是bitcode文件,仅伪装成方针文件)并搜集符号信息。
  • 接下来,链接器运用大局符号表解析符号。找到未界说的符号,替换weak符号等等。
  • 按照解析的成果,告诉履行LTO的库文件(默许是libLTO.dylib)那些符号是需求的。紧接着,链接器调用优化器和代码生成器,回来经过合并bitcode文件并应用各种优化进程而创立的方针文件。然后,更新内部大局符号表。
  • 链接器持续运转,直到生成可履行文件。
58同城iOS包大小治理工具解密

LTO有以下两种办法:

  • Full LTO是将每个单独的方针文件中的一切LLVM IR代码组合到一个大的module中,然后对其进行优化并像平常相同生成机器代码。

  • Thin LTO是将模块分开,可是根据需求能够从其他模块导入相关功用,并行进行优化和机器代码生成。

主工程中装备LTO为Monolithic,即LLVM_LTO = YES,如下图所示。

58同城iOS包大小治理工具解密

假如含有cocoapods子工程,需求对子工程进行LTO装备,在Podfile中装备即可,如下所示。

post_install do |installer|
  installer.sandbox.target_support_files_root.glob('**/*.xcconfig').each do |xcconfig_file|
    config = Xcodeproj::Config.new(xcconfig_file)
    config.attributes['LLVM_LTO'] = 'YES'
    config.other_linker_flags[:simple] << "-Wl,-mllvm,--enable-machine-outliner=always,-mllvm,--machine-outliner-reruns=1"
    config.save_as(xcconfig_file)
  end
end

3 东西的运用

主页选择App一键减肥

App一键减肥东西的运用过程

将编译好的工程.app路径拖入输入框中, 点击确诊,就会输出App中OC、Swift无用类、包巨细优化主张、无用资源以及重复资源等信息, 一键为App供给包巨细优化供给主张,具体操作与展现如下图所示:

3.1 功用简介

  • 经过东西主页->App一键体检进入,拖入编译成功的.app到输入框,点击确诊,就能够直观展现App包巨细的散布状况, 分为可履行文件巨细、资源巨细(Assets)、其他资源巨细等,详见下图:
58同城iOS包大小治理工具解密
58同城iOS包大小治理工具解密
  • 点击包巨细散布区域,可展现App包巨细散布的具体信息;无用类的具体信息能够经过点击下拉列表以及右侧的检查文件检查具体的无用类名称,详见下图:
58同城iOS包大小治理工具解密
58同城iOS包大小治理工具解密
  • 段搬迁技能能够在低端系统下大幅下降包巨细,LTO技能苹果会在编译期间对汇编代码做大局优化,具体运用办法可点击【检查装备】,详见下图;
58同城iOS包大小治理工具解密
58同城iOS包大小治理工具解密
  • 图片在Asset中Apple会根据设备分发不同图片,东西可主动检测App中未在Asset中的图片,详见功用简介右图[5];

  • 无用图片检测, 检测App中或许的无用图片,详见功用简介右图[6];

  • 检测App中重复资源, 可经过移除重复资源的办法下降包巨细,详见功用简介右图[7]。

4 总结

各APP在事务迭代中包巨细都滚雪球似的不断在增大,而APP巨细也影响着用户增量,因而APP减肥是一个持续的进程。包巨细检测在现已在58同城各个APP以及各事务线中运用,已贯穿集团内研制的各个阶段,成为了衡量集团内库巨细以及各事务线事务迭代包增长巨细的标准东西。当然,WBBlades东西也是在不断地完善中,这儿感谢业界大佬们一直以来提出的名贵主张。

本文说到的WBBlades东西,是58同城iOS团队的开源项目,其功用包括包巨细检测、无用类检测、崩溃解析、静态库巨细检测等。如您有所得,欢迎star:github.com/wuba/WBBlad… 。

参阅文档

WBBlades

Swift Hook新思路–虚函数表

反汇编技能计划之capstone

LLVM LTO

ld源码

今日头条段搬迁