美文网首页iOS开发者进阶手机移动程序开发
iOS 安装包的瘦身 (持续完善中...)

iOS 安装包的瘦身 (持续完善中...)

作者: __Mr_Xie__ | 来源:发表于2019-06-28 16:43 被阅读54次

    前言

    随着 app 版本的迭代,app 功能可能会越来越多,此时 app 打出来的包也会越来越大,由于 Apple 对安装包大小的有限制(具体参见 Apple文档),所以这就涉及到app安装包的瘦身。
    App Store 安装包是由 资源可执行文件 两部分组成,安装包瘦身也是从这两部分进行。

    资源瘦身

    资源瘦身主要是去掉 无用资源压缩资源 ,资源包括 图片音视频文件配置文件 以及 多语言 wording。无用资源是指资源在工程文件里,但没有被代码引用。检查方法是,用资源关键字(通常是文件名,图片资源需要去掉 @2x @3x),搜索代码,搜不到就是没有被引用。当然,有些资源在使用过程中是拼接而成的(如 loading_xxx.png ),需要手工过滤。

    经验:可以把工具使用和手工过滤想结合。

    • 如何优雅的清理掉那些无用资源
      一个项目开发得越久,添加的功能模块也就越多,相应地,也会慢慢引入大量图片等资源。但是,在移除一些不再使用的模块的时候,开发者往往会忘记把该模块所对应的图片资源一起删除,因为源码和资源是分离的。长久以来,项目中就会存在大量没被使用的资源文件。
      具体方法无非是,一个一个地复制资源文件名,然后在 XCode 中全局查找该字符串,如果结果为 0 ,那么这个资源很可能就没有被使用。为什么说很可能?因为在代码中,有可能通过字符串拼接的方式使用了这个资源,而这种情况是没办法通过字符串匹配查找出来的。
      于是,我们需要这么一款工具:能够迅速找出工程中所有没被使用的资源文件。
      工具一Unused

    Use this useful utility tool to check what image resources are not being used in your Xcode projects. Very useful to reduce your bundle size by showing you what images are not used!

    优点:工具 Unused脚本 的调用做了封装,通过界面可以配置一定的信息,然后比较清晰的输入结果。
    缺点:不够智能,不够通用,速度太慢,结果不正确。

    工具二LSUnusedResources
    或直接下载 LSUnusedResources.app.zip


    LSUnusedResources 很大程度上受 工具 Unused 的影响,比如界面、交互,以及部分代码。但是,本工具在核心代码上做了优化,使其在搜索的速度、结果的正确上都有了很大的提高。

    LSUnusedResources工具核心思想,简述如下:

    • 查找:选定的目录下的所有资源文件。这一步与工具 Unused 区别不大,都是调用 find 命令查找指定后缀名的文件。
    • 匹配:与工具 Unused 不同,LSUnusedResources 不是对每个资源文件名都做一次全文搜索匹配,因为加入项目的资源太多,这里会导致性能快速下降。而 LSUnusedResources 只是针对源码、XibStoryboardplist 等文件,先全文搜索其中可能是引用了资源的字符串,然后用资源名和字符串做匹配。

    LSUnusedResources匹配结果
    工具 Unused 会把大量实际上有使用的资源,当做未使用的资源输出。LSUnusedResources 则不会出现这样的问题,并且使得结果更加优化。举例说明:

    icon_tag_0.png
    icon_tag_1.png
    icon_tag_2.png
    icon_tag_3.png
    

    然后用字符串拼接的方式在代码中引用:

    NSInteger index = random() % 4;
    UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:@"icon_tag_%d", index]];
    

    icon_tag_x.png 是不应该被当做未使用的资源的,只是以一种比较隐晦的方式间接引用了,所以不应该出现在结果列表中,LSUnusedResources 工具做到了。

    • 压缩资源
      资源压缩主要对 png 进行无损压缩,用的是 ImageOptim 工具和 compress 命令(需要安装 XQuartz-2.7.5.dm 插件)。不建议对资源做有损压缩,有损压缩需要设计一个个检查,通常压缩后效果不尽人意。
      ImageOptim压缩结果

    插曲

    在聊可执行文件的瘦身之前,先介绍一下 XcodeLink Map FileLinkMap 文件是 Xcode 产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括 代码段(__TEXT)数据段(__DATA) 的分布情况。只要设置 Project -> Build Settings -> Write Link Map FileYES ,并设置 Path to Link Map Filebuild 完后就可以在设置的路径看到 LinkMap 文件了:



    注:想要了解更多,具体可以参见我的文章 Xcode的Link Map File

    可执行文件瘦身

    回到我们的可执行文件瘦身问题,LinkMap 文件可以帮助我们寻找优化点。

    1. 无用方法检测思路

    以往 C++ 在链接时,没有被用到的类和方法是不会编进可执行文件里。但 Objctive-C 不同,由于它的动态性,它可以通过类名和方法名获取这个类和方法进行调用,所以编译器会把项目里所有 OC 源文件编进可执行文件里,哪怕该类和方法没有被使用到。

    结合 LinkMap 文件的 __TEXT.__text ,通过正则表达式( [+|-][.+\s(.+)] ),我们可以提取当前可执行文件里所有 objc类方法和实例方法( SelectorsAll )。再使用 otool 命令 otool -v -s __DATA __objc_selrefs 逆向 __DATA.__objc_selrefs 段,提取可执行文件里引用到的方法名( UsedSelectorsAll ),我们可以大致分析出 SelectorsAll 里哪些方法是没有被引用的( SelectorsAll-UsedSelectorsAll )。扫描脚本( py ):

    import os
    import re
    
    outPath = "/Users/luph/Documents/sizetj/" #输出目录
    mathoFilePaht = "/Users/luph/Documents/sizetj/Pro" #可执行文件
    linkmapPath = "/Users/luph/Documents/sizetj/Pro-LinkMap-normal-arm64.txt"
    selrefsFile =  outPath+"/selrefs.txt" #引用sel文件
    cmd = "otool -v -s __DATA __objc_selrefs "+ mathoFilePaht +" >> "+selrefsFile
    os.system(cmd) #逆向selrefs段
    
    linkmapContent = open(linkmapPath,encoding="utf8", errors='ignore').read()
    pattern = re.compile(r'[+|-]\[\w+ \w+\]') 
    selall = pattern.findall(linkmapContent)
    
    selrefsF = open(selrefsFile,encoding="utf8", errors='ignore')
    selrefsList = []
    for line in selrefsF.readlines():
        if '__objc_methname' in line:
            line = line.strip("\n");
            lineSplit = line.split(":")
            if  len(lineSplit)  > 0:
                selrefs = ""
                lineSplit.reverse()
                for subStr in lineSplit:
                    if len(subStr) > 0:
                        selrefs = subStr
                        break
                if len(selrefs) > 0:
                    selrefsList.append(selrefs)
    selrefsF.close()   
    
    output = open(outPath+"result.txt", 'w')
    for sel in selall:
        print("正在扫描【{0}】".format(sel))
        selMth = sel.replace("+",'')
        selMth = selMth.replace("-",'')
        selMth = selMth.replace("[",'')
        selMth = selMth.replace("]",'')
        selL = selMth.split(" ")
        selMth = selL[1]
        isUse = False
        for selref in selrefsList:
            if  selref == selMth:
                isUse = True
                break 
        if not isUse:
            print("发现无用方法【{0}】".format(sel))
            output.write("{0}\n".format(sel))  
         
    output.close()
    print("扫描结束")
    

    注:

    • 系统 APIProtocol 可能被列入无用方法名单里,如 UITableViewDelegate 的方法,我们只需要对这些 Protocol 里的方法加入白名单过滤即可。
    • 另外第三方库的无用 selector 也可以这样扫出来的。
    • 你也可以用一下 工具 来扫.
    2. 查找无用oc类

    查找无用oc类有三种方式:

    • 一种是类似于查找无用资源,通过搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]"等关键字在代码里是否出现;
    • 另一种是通过otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段来获取当前所有oc类和被引用的oc类,两个集合相减就是无用oc类;
    • 最后一种使用fui工具扫描;
    3. 扫描重复代码

    可以利用第三方工具simian扫描。

    4. 其他

    从安装包的瘦身角度来说,对于语言选择这块来说,不推荐使用 Swift,不论纯 Swift 还是 混编,任何一个包含有 Swift 代码的 App 都有的一个为了支持 Swift 的动态库集合,在10M 左右。如果你使用 Objective - C 完全不用这个东西。

    更多相关

    1. App Thinning

    据Apple官方文档的介绍,App Thinning主要有三个机制:

    • Slicing
      开发者把App安装包上传到AppStore后,Apple服务会自动对安装包切割为不同的应用变体(App variant),当用户下载安装包时,系统会根据设备型号下载安装对应的单个应用变体。(你不需要做什么,iOS9.0.2以上就支持)


    • Bitcode
      开启Bitcode编译后,可以使得开发者上传App时只需上传Intermediate Representation(中间件),为二进制数据表示的格式的中间码,而非最终的可执行二进制文件。 在用户下载App之前,AppStore会自动编译中间件,产生设备所需的执行文件供用户下载安装。也就是当我们提交程序到 App Store上时, Xcode 会将程序编译为一个中间表现形式( bitcode )。然后 App store 会再将这个 Bitcode 编译为可执行的64位或32位程序。苹果会根据下载应用的用户的手机指令集类型生成只有该指令集的二进制,进行下发



      所以,通过这个方式,我们可以做到架构级别的App Slicing。

    然而,一个很常见的误区是认为使用 bitcode 能优化包大小,其实启用 bitcode 作用并不大。实际上 bitcode 和包大小半毛钱关系都没有,它仅仅是把编译的最后一步留给苹果,这样苹果就可以在优化编译器后,再次将我们的应用打包,从而让历史应用也能享受到新技术
    文档 里可看到

    In fact, app slicing handles the majority of the app thinning process. ‘App Slicing’ feature finally switched on in iOS 9.0.2

    说明slicing才是主要处理 app thinning的而且该功能需要在iOS9.0.2以上才支持(iOS9.0中被关闭了,因为一个iCloud的bug)。实际上Bitcode,做的事情是指令集优化。根据你设备的状态去做编译优化,进而提升性能。所以Bitcode对包的大小优化起不到什么本质上的作用。

    注意点
    1.开启 Bitcode 编译后,编译产生的 .app 体积会变大(中间代码,不是用户下载的包),且 .dSYM 文件不能用来崩溃日志的符号化(用户下载的包是 Apple 服务重新编译产生的,有产生新的符号文件)
    2.通过 Archive 方式上传 AppStore 的包,可以在Xcode的Organizer工具中下载对应安装包的新的dSYM符号文件。或者iTunes Connect上下载对应构建包的dSYM(需消除混淆)
    详情见 文档

    • On-Demand Resources
      On-Demand Resources(即按需资源)是指开发者对资源添加标签上传后,系统会根据App运行的情况,动态下载并加载所需资源,而在存储空间不足时,自动删除这类资源。
      这可能在游戏中应用场景会多一些。你可以用 tag 来组织像图像或者声音这样的资源,比如把它们标记为 level1,level2 这样。然后一开始只需要下载 level1 的内容,在玩的过程中再去下载 level2。或者也可以通过这个来推后下载那些需要内购才能获得的资源文件。



      这种机制对于大多数APP来讲,看起来更像是按需加载网络图片,并作缓存处理。而On-Demand Resources只是将这个服务交由苹果来处理, 个人觉得多少显得鸡肋。

    Author

    如果你有什么建议,可以关注我的公众号:iOS开发者进阶,直接留言,留言必回。

    参考

    相关文章

      网友评论

        本文标题:iOS 安装包的瘦身 (持续完善中...)

        本文链接:https://www.haomeiwen.com/subject/gawccctx.html