看了这篇,面试官问你APP体积优化再也不用WTF了

long time no see,最近在总结一些平(应)常(付)用(面)到(试)的知识点,今天就跟大家聊了聊App体积优化这个事儿。

1.为什么要做体积瘦身

别问!问就是为了应付面试。

哈哈,开个玩笑。大家生活中都会遇到一个场景,在某个需要紧急打开App的时候,发现使用的App半天打不开!WTF!而另外一款相同功能的App却可以瞬间打开。哪个App能够挽留更多的用户就不言而喻了吧!

借用某个游戏里边人物的一句话:"时间就是金钱,我的朋友!"

2.我们都能干什么?

下边我们先查看一个的思维导图:

 

 

思维导图已经总结目前我已经知道的并且可以落地的优化方式。

如图所示APP体积优化包括两部分:资源瘦身和代码瘦身。

下面我将使用APPReduction这个简单的demo实地操作一下。需要的同学可以到gayhub下载一下。

3. 具体实施

3.1 资源瘦身

  • 删除资源

    因为旷日持久的业务代码堆砌,工程内很可能会堆积许多无用的图片,而这些图片却能实实在在的增加App的体积。而我们完全可以借助工具LSUnusedResources进行资源文件的删除工作。

 

 

RedutionViewController中,configImageTest方法中你会找到image图片的代码调用

- (void)configImageTest{
    
    [UIImage imageNamed:@"cooker"];
    [UIImage imageNamed:@"driver"];
    [UIImage imageNamed:@"function"];
    [UIImage imageNamed:@"header"];
//    [UIImage imageNamed:[@"think_"stringByAppendingFormat:@"005"]];
//    [UIImage imageNamed:[@"think_"stringByAppendingFormat:@"006"]];
//    [UIImage imageNamed:[@"think_"stringByAppendingFormat:@"007"]];
    
}
复制代码

当我们使用LSUnusedResources进行图片资源检察的时候会发现未使用的图片。

 

 

但是这里需要注意最好进行一下手动的检察避免出现误删的情况,并且如果代码仅仅是注释掉,程序并不会认为资源是废弃的。

  • 压缩资源

    请以合理且合法的方式多跟UI多谈谈,在切图片的时候请按需要切图,不必要每一张都是高清无码,在可接受的范围内可以压缩资源图片!而且笔者认为所有跟图片相关的改变质量的问题都需要经手UI,切记不要自己瞎搞。

  • 大图片资源

    不经常用到的大图资源可以采取下载的方式加载到APP上,不是非要打包到ipa里边!能跟产品聊聊的问题别死磕代码

3.2 代码瘦身

当我们的App被打包成ipa的时候,代码会被打包成一个一个个的.o文件,而这些.o文件组成了MachO,而系统在编译MachO文件的时候会生成一个附带的文件LinkMap。

3.2.1 LinkMap

  • LinkMap的组成

    LinkMap由Object File、Section、Symbol三部分组成,描述了工程所有代码的信息。可以根据这些信息针对性的去优化空间。

  • LinkMap的获取

    1.在XCode中开启编译选项Write Link Map File \nXCode -> Project -> Build Settings -> 把Write Link Map File设置为YES

    2.在XCode中开启编译选项Write Link Map File \nXCode -> Project -> Build Settings -> 把Path to Link Map File的地方设置好地址

    3.运行项目在地址位置将生成.txt的文件

  • LinkMap的分析

    1.借助工具LinkMap解析工具,我们可以分析每个类占用的大小

 

 

2.针对性的进行代码的体积的优化,比如三方库占用空间巨量,有没其他的替代方案。在取舍两个相同库的时候也可以根据体积的比重做出取舍。

看到这里我们已经可以从宏观的角度上获取到需要优化哪些部分的代码,但是微观角度哪些是无用的类哪些是无用的方法,需要我们进一步从MachO的层面上去分析。

3.2.2 MachO分析

MachO文件可以说是App编译之后的最重要的部分,通过MachOView这个软件我们可以更加直观看到MachO的组成。如果你的MachOView运行的时候出现崩溃请按照这篇文章进行修改

  • MachO的组成

 

 

__objc_selrefs:记录了几乎所有被调用的方法

__objc_classrefs和__objc_superrefs:记录了几乎所有使用的类

__objc_classlist:工程里所有的类的地址

  • 删除无用的类

    MachO文件中__objc_classrefs段记录里了引用类的地址,__objc_classlist段记录了所有类的地址,我们可以认为取两者的差值就可以获得未使用类的地址,然后进行符号化,就可以取得未使用类的信息。

    大家可以使用classunref这个工具实现未使用类的查找。

    如果对实现感兴趣的同学可以拜读大佬的文章iOS代码瘦身实践:删除无用的类

  • 删除未使用的方法

    当我们将无用的类删除完毕之后,在已经使用的类里边很有可能依然会有未使用的方法。

    前边我们已经提到过LinkMap中保存了工程的信息,而我们所有已经被包含到项目中的方法可以通过LinkMap获取。

    classunref的启发下,笔者利用python实现了未使用方法的自动化方式

    因为py实在不太熟悉加上笔者比较懒,请大家忽略一些语法、接口设计不规范等等的问题

    1.使用指令grep '[+|-]\[.*\s.*\]' xxx-linkMap.txt指令我们得到所有被包含到工程到项目中的代码

    // 获取所有的方法
    def method_readRealization_pointers(linkMapPath,path):
    # all method
    lines = os.popen("grep '[+|-]\[.*\s.*\]' %s" % linkMapPath).readlines()
    // 需要忽略的方法
    lines = method_ignore(lines,path);
    pointers = set()
    for line in lines:
        line = line.split('-')[-1].split('+')[-1].replace("\n","")
        line = line.split(']')[0]
        line = str("%s]"%line)
        pointers.add(line)
    if len(pointers) == 0:
        exit('Finish:method_readRealization_pointers null')
    print("Get all method linkMap pointers...%d"% len(pointers))
    return pointers
    复制代码

    2.考虑到大家项目中使用了大量的三方库,而三方库的方法有许多并未使用,所以通过method_ignore方法进行忽略,这样获取的差值的集合中就不会包括三方库的未使用方法

    def method_ignore(lines,path):
      print("Get method_ignore...")
      effective_symbols = set()
      // 获取所有需要忽略类名的前缀(例如YYModel 会以前缀YY的方式作出忽略)
      prefixtul = tuple(class_allIgnore_Prefix(path,'',''))
      getPointer = set()
      
      // 此处是为了忽略Setter Getter方法
      for line in lines:
          classLine = line.split('[')[-1].upper()
          methodLine = line.split(' ')[-1].upper()
          if methodLine.startswith('SET'):
             endLine = methodLine.replace("SET","").replace("]","").replace("\n","").replace(":","")
             print("methodLine:%s endLine:%s"%(methodLine.lower(),endLine.lower()))
             if len(endLine) != 0:
                getPointer.add(endLine)
      getPointer = list(set(getPointer))
      getterTul = tuple(getPointer)
      
      for line in lines:
          classLine = line.split('[')[-1].upper()
          methodLine = line.split(' ')[-1].upper()
          if (classLine.startswith(prefixtul)or methodLine.startswith(prefixtul)  or methodLine.startswith('SET') or methodLine.startswith(getterTul)):
              continue
          effective_symbols.add(line)
      
      if len(effective_symbols) == 0:
          exit('Finish:method_ignore null')
      return effective_symbols;
    
    复制代码

    3.使用指令otool -v -s __DATA__objc_selrefs指令我们可以得到所有已经被实现的方法

    def method_selrefs_pointers(path):
      # all use methods
      lines = os.popen('/usr/bin/otool -v -s __DATA __objc_selrefs %s' % path).readlines()
      pointers = set()
      for line in lines:
           line = line.split('__TEXT:__objc_methname:')[-1].replace("\n","").replace("_block_invoke","")
           pointers.add(line)
      print("Get use method selrefs pointers...%d"% len(pointers))
      return pointers
      
    复制代码

    3.利用步骤1和步骤2的差值可以获取到还未使用的方法。

    def method_remove_Realization(selrefsPointers,readRealizationPointers):
      if len(selrefsPointers) == 0:
         return readRealizationPointers
      if len(readRealizationPointers) == 0:
         return null
      methodPointers = set()
      for readRealizationPointer in readRealizationPointers:
          newReadRealizationPointer = readRealizationPointer.split(' ')[-1].replace("]","")
          methodPointers.add(newReadRealizationPointer)
      unUsePointers = methodPointers - selrefsPointers;
    
      dict = {}
      for unUsePointer in unUsePointers:
          dict[unUsePointer] = unUsePointer
      
      for readRealizationPointer in readRealizationPointers:
          newReadRealizationPointer = readRealizationPointer.split(' ')[-1].replace("]","")
          if dict.has_key(newReadRealizationPointer):
              dict[newReadRealizationPointer] = readRealizationPointer
              str = dict[newReadRealizationPointer]
      
      return list(dict.values())
    
    复制代码

    感兴趣的同学可以在这里下载到修改版的 ONLClassMethodUnref

3.2.3 AppCode

如果你的工程不够巨大,借助AppCode这个工具的静态分析也可以查找到未使用的代码。方法极为简单打开AppCode->选择Code->点击Inspect Code---等待静态分析

但是笔者还是在这里需要作出提醒,即使你用到了上边的所有方式,基于动态语言的特性,我们仍然不能够找出所有未使用的代码,并且在删除代码的时候仍然需要小心翼翼!切勿多删!记得做回归测试!

> 作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:761407670 进群密码'博客',不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

提供逆向安防、Swift、算法、架构设计、多线程,网络进阶,还有底层、音视频、Flutter等资料

 

 

参考资料

1.iOS代码瘦身实践:删除无用的类

2.MachOView运行的时候出现崩溃请按照这篇文章进行修改



posted @ 2020-05-28 15:31  浪人不归乡  阅读(381)  评论(0编辑  收藏  举报