iOS App瘦身记录

作者: keyser_fayee | 来源:发表于2019-05-13 20:13 被阅读0次
    问题:

    App Store规定安装包大小超过150MB的App只能在WIFI环境下载。现在项目App包已经超过这条线,这意味着可能将损失大量用户,需要对其进行瘦身

    App现状

    archive后,ipa包大小为132.3MB,.app大小为166MB,Mach-O大小为86.1MB,Assets.car大小为61.4MB

    ipa包:archive后生成的文件,实际为一种压缩包,解压后包含.app文件和symbols信息。根据app store规定,.app文件超过150MB则无法进行OTA升级
    app文件:实际的应用程序包,里面包含app同名的Mach-O可执行文件,Asset.car、.nib、.bundle、Localizable.strings等资源文件,_CodeSignature文件夹包含签名信息。
    Mach-O文件:代码编译链接后生成的可执行文件,包含支持的所有cpu指令集。可以通过Link-Map分析其中所有方法和类所占空间的大小。
    Assets.car:xcassets图片文件。
    因此,对App瘦身实际上就是减小.app文件的大小

    查看.app包内容规划此次App瘦身的目标:针对大于100Kb的文件进行瘦身,包含以下两点:
    1.整理资源文件,包括文件压缩与无用资源的删除
    2.整理可执行文件,包括删除无用类和无用函数

    .app文件包内容

    瘦身方法

    1.LSUnusedResources查找并删除无用文件
    先勾选"Ignore similar name"过滤掉以"tag_%d"命名的文件后再去掉勾选,多筛几遍
    瘦身效果:129.1MB;app文件大小:160.8MB; X1可执行文件:86.1MB;Assets.car:56.2MB

    2.Link-Map分析并删除无用三方库
    使用Link-Map分析工具得出每个类或者库所占用的空间大小,可以快速定位需要优化的类或静态库。
    瘦身效果:123.7MB;app文件大小:156.8MB; X1可执行文件:84.1MB;Assets.car:56.2MB

    3.用LaunchScreen.storyboard替代启动图
    上图.app包中PNG图片占用大量空间,全部为LaunchImage中的全尺寸启动图。删除后用storyboard替代,注意为UI控件添加合适约束以适应不同尺寸的手机。
    瘦身效果:ipa包大小:116.1MB;app文件大小:147.7MB; X1可执行文件:84.1MB;Assets.car:56.2MB

    4.选择合适的cpu指令集进行编译
    使用MachOView查看app中的Mach-O文件,armv7占了一小半

    mach-o文件体积
    9012年如果追求app体积最优解,可以去掉对armv7的支持(iphone4,iphone4s),当然指令集是向下兼容的,如果想全尺寸支持可以选择armv7(iphone5,iphone5c向下兼容armv7)和arm64。这里我选择去掉armv7。
    瘦身效果:ipa包大小:80.4MB;app文件大小:107.5MB; X1可执行文件:45.9MB;Assets.car:56.2MB

    5.python脚本查找并删除无用类
    分析了两天破解版和未破解版的AppCode,得出的unused code只有import问题,后来参考这里,一个简单的脚本分析出无用的类。

    # -*- coding: UTF-8 -*-
    #!/usr/bin/env python
    # 使用方法:python py文件 Xcode工程文件目录
    
    import sys
    import os
    import re
    
    if len(sys.argv) == 1:
        print '请在.py文件后面输入工程路径'
        sys.exit()
    
    projectPath = sys.argv[1]
    print '工程路径为%s' % projectPath
    
    resourcefile = []
    totalClass = set([])
    unusedFile = []
    pbxprojFile = []
    
    def Getallfile(rootDir):
        for lists in os.listdir(rootDir):
            path = os.path.join(rootDir, lists)
            if os.path.isdir(path):
                Getallfile(path)
            else:
                ex = os.path.splitext(path)[1]
                if ex == '.m' or ex == '.mm' or ex == '.h':
                    resourcefile.append(path)
                elif ex == '.pbxproj':
                    pbxprojFile.append(path)
    
    Getallfile(projectPath)
    
    print '工程中所使用的类列表为:'
    for ff in resourcefile:
        print ff
    
    for e in pbxprojFile:
        f = open(e, 'r')
        content = f.read()
        array = re.findall(r'\s+([\w,\+]+\.[h,m]{1,2})\s+',content)
        see = set(array)
        totalClass = totalClass|see
        f.close()
    
    print '工程中所引用的.h与.m及.mm文件'
    for x in totalClass:
        print x
    print '--------------------------'
    
    for x in resourcefile:
        ex = os.path.splitext(x)[1]
        if ex == '.h': #.h头文件可以不用检查
            continue
        fileName = os.path.split(x)[1]
        print fileName
        if fileName not in totalClass:
            unusedFile.append(x)
    
    for x in unusedFile:
        resourcefile.remove(x)
    
    print '未引用到工程的文件列表为:'
    
    writeFile = []
    for unImport in unusedFile:
        ss = '未引用到工程的文件:%s\n' % unImport
        writeFile.append(ss)
        print unImport
    
    unusedFile = []
    
    allClassDic = {}
    
    for x in resourcefile:
        f = open(x,'r')
        content = f.read()
        array = re.findall(r'@interface\s+([\w,\+]+)\s+:',content)
        for xx in array:
            allClassDic[xx] = x
        f.close()
    
    print '所有类及其路径:'
    for x in allClassDic.keys():
        print x,':',allClassDic[x]
    
    def checkClass(path,className):
        f = open(path,'r')
        content = f.read()
        if os.path.splitext(path)[1] == '.h':
            match = re.search(r':\s+(%s)\s+' % className,content)
        else:
            match = re.search(r'(%s)\s+\w+' % className,content)
        f.close()
        if match:
            return True
    
    ivanyuan = 0
    totalIvanyuan = len(allClassDic.keys())
    
    for key in allClassDic.keys():
        path = allClassDic[key]
        
        index = resourcefile.index(path)
        count = len(resourcefile)
        
        used = False
        
        offset = 1
        ivanyuan += 1
        print '完成',ivanyuan,'共:',totalIvanyuan,'path:%s'%path
        
        
        while index+offset < count or index-offset > 0:
            if index+offset < count:
                subPath = resourcefile[index+offset]
                if checkClass(subPath,key):
                    used = True
                    break
            if index - offset > 0:
                subPath = resourcefile[index-offset]
                if checkClass(subPath,key):
                    used = True
                    break
            offset += 1
        
        if not used:
            str = '未使用的类:%s 文件路径:%s\n' %(key,path)
            unusedFile.append(str)
            writeFile.append(str)
    
    for p in unusedFile:
        print '未使用的类:%s' % p
    
    filePath = os.path.split(projectPath)[0]
    writePath = '%s/未使用的类.txt' % filePath
    f = open(writePath,'w+')
    f.writelines(writeFile)
    f.close()
    

    删除项目中无用类。
    瘦身效果:ipa包大小:80.1MB;app文件大小:107.2MB; X1可执行文件:45.6MB;Assets.car:56.2MB

    6.xcassets图片压缩
    项目使用xcassets管理图片
    1)使用ImageOptime工具进行后,编译时xcode会还原压缩后的png图片,需要设置COMPRESS_PNG_FILESSTRIP_PNG_TEXTNo
    2)TinyPng对大尺寸PNG图片进行压缩,免费版每日有压缩数量限制。项目压缩130Kb以上png图片,再替换xcassets中原图。
    瘦身效果:ipa包大小:69.5MB;app文件大小:96.4MB; X1可执行文件:45.6MB;Assets.car:45.3MB

    小结

    总结就是资源和代码两方面下手,xcode编译设置其实作用感觉不太大,其实最关键的还是需要结合自身项目,分析其构成,来找到适合自己项目的瘦身方法。

    参考

    ipa包大小优化

    iPhone安装包的优化

    iOS APP安装包瘦身实践

    包大小:如何从资源和代码层面实现全方位瘦身?

    相关文章

      网友评论

        本文标题:iOS App瘦身记录

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