iOS built-thin picture

A, iOS centralized way of built-in resources

1.1 The pictures stored in the bundle

This is a very common way to classify different types of documents on the project in each bundle, the project not only clean but also to achieve isolation resources purposes. The use of bundle loading is [UIImage imageNamed: "xx.bundle / xx.png "].

This approach has obvious drawbacks:

  1. iOS will not be stored in a compressed, resulting in an increase of the volume of applications.

  2. Use bundle pictures stored abandoned APP thinning. Obvious manifestation is the same as 2 times and 3 times the screen phone screen mobile phone to download the application packet size. If we can achieve APP thinning, so often twice the screen size of a mobile phone package will be less than 3 times the screen of the phone, played the purpose of optimizing the difference.

In the research process found that closely related to the volume and the number of pictures resources applications. In other words, iPhone rom presence of 4K aligned, a 498B-size image also occupies 4KB size in the application package. Therefore, every project add an image to at least increase the 4KB.

Let's confirmed. First, create an empty application, its size on iPhone7 is 131KB, following the introduction of contrast before and after pictures of a 3KB:

Or more has not elapsed App Store on-line authentication only by local real machine test run for reference.

1.2 .ttf font files replace icon

Use font files replace the picture is also a built-in way more common resources. Many applications use this kind of program, such as Taobao, iQIYI other well-known applications.

The advantage of using the font file is obvious, if APP in a picture is relatively large, so in order to ensure clarity, UI may provide a relatively large icons. Use font file to avoid this problem, but do not have the import and @ 3x @ 2x images, set font files can ensure clarity UI.

Font file is simple to use, but the use of methods and use png images are very different, because the string font files actually show icon are transferred to the UTF8 encoding. So when we need to show an icon when no longer of use UIImageView, but UILabel.

UILabel * iconLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
iconLabel.font = [UIFont fontWithName:@"icomoon" size:50];
iconLabel.text = [NSString stringWithUTF8String:"\ue902"];

由于使用了字体来替代图片,所以可以通过设置字体的颜色来改变图标的颜色。之前经常会遇到一个场景,如两个一模一样的图标但是由于颜色不同,UI 就需要提供两套图片,每套图片中包含 @2x 和 @3x 图片。如果采用了字体替代简单的图标,那么 UI 只需要提供一套字体即可,并且拉伸后也不会失真。

优点:

  1. 可以降低应用图片内置资源的体积。
  2. 可以随意缩放和修改颜色。

缺点:

  1. 图标的查找和替换比较麻烦,不如直接使用图片那样简单。
  2. 有些情况无法替换之前存在的图片,只能起到缩小增量的目的,无法减小全量。

任何一种需要大刀阔斧改革的优化都是一种不明智的行为。

1.3 图片存在 Assets.xcassets

使用 Assets.xcassets 是苹果推荐的一种方式。Assets.xcassets 是 iOS7 推出的一种图片资源管理工具,将图片内置到Assets.xcassets 下系统会对图片资源进行压缩,并且支持 APP thinning。

二、优化

项目优化不能脱离场景,很多很好的方案由于场景的限制并不能起到优化的作用。

为了达到跨团队快速开发的目的,项目很早就利用 cocoapods 实现组件化。项目中存在多个业务 pod,每个 pod 都有各自的团队维护,各个团队的代码彼此不开放,各个 pod 最终会被编译为 .a 的形式。

与 .a 相对应的是 .framework,它们之间有一个重要的区别就是资源的问题。.framework 中可以存放资源,但 .a 不可以,因此生成 .a 的 pod 下的资源会被转移到 main bundle 下,这为资源冲突造成了隐患。采用的 bundle 管理资源大大降低了资源冲突的可能性,因为 bundle 名很少会重复。

优化的前提之一也是不破坏这种组件化开发的模式,换句话说也就是各个业务线不产生资源耦合、业务线的 RD 不必担心彼此资源的冲突、业务 Pod 下的资源文件彼此隔离。

先要抛出两个问题:

  1. cocoapods 是否支持使用 Assets.xcassets。
  2. 各个 pod 维护自己的 Assets.xcassets 会不会造成资源冲突。

为了弄清楚上面两个问题,先要看下 podspec 的几个重要参数:

s.source_files :源文件路径。

s.public_header_files :表明了哪些路径下的文件可以在 framework 外被引用。

s.resources :资源文件路径及文件类型。

s.resource_bundles :资源文件路径及类型,同时资源文件会被打成 bundle。(推荐使用)。

实验发现各个 pod 下都可以创建自己的 xcassets,因此问题 ① 确定。

如果我们在各个业务 pod 下都创建 .xcassets 文件内置图片,那么 cocoapods 的脚本会在编译时将各个目录下的 xcassets 文件内容提取出来,合并到一个 xcassets 中并生成一个 .car 文件。这样的话如果资源文件重名,那么很可能其中某一个文件会被覆盖替换。因此我们主要是要解决问题 ②。

查看 podspec 的写法发现 s.resource_bundles 貌似是我们所需要的法宝。

最终打包结果很理想,确实能够生成 Demo.bundle,并且 bundle 下存在 Assets.car。

运行发现通过 [UIImage imageNamed:@"Demo.bundle/1"];加载不出来图片。必须使用 [UIImage imageNamed:@"1" inBundle:bundle compatibleWithTraitCollection:nil]; 才能加载出来。也就是说如果 Assets.car 不在 main bundle 下,那么加载图片需要指定 bundle。

既然需要指定 bundle 加载图片,那么如何获取这个 bundle 呢?换句话说如何才能低成本的将项目中的图片放到特定 bundle 下的 Assets.car 文件中呢?对此我们提出了一个解决方案:

  1. 在 pod 下新建一个空文件夹。找出该 pod 存放图片的所有 bundle,在新建文件夹下创建与 bundle 数量相等的 Asset。
  2. 修改 podspec 文件,设置 resource_bundles 将 Asset 指定为资源,并指定 bundle 名称,如 A.bundle,其对应的 Asset 最终资源 bundle 为 A_Asset.bundle。
  3. 新增方法 imageWithName:,从符合 xx.bundle/yy.png 特征的参数中获取 bundle 名和图片名 xx_Asset.bundle 和 yy.png,获取图片并返回。
  4. 查找并全部替换 imageNamed: 和 imageWithContentOfFile: 为 imageWithName:。

只要能拿到原来代码中 imageNamed: 的参数就能知道现在图片存在哪个 bundle 下,这样就能通过 imageNamed:inBundle: 获取到图片,其思路如下图所示:

看到这里已经应该能遇见这种优化的成本了。加载图片都需要指定 bundle 也就意味着成千上万处的 API 需要修改。我们最初探讨到这里的时候首先想到的是脚本,但是这个方案很快就被否定了,因为项目中存在大量的 XIB,XIB 中设置图片我们无法通过脚本替换 API。

为了解决 XIB 设置图片的问题,我们首先想到了 AOP。通过 hook Xib 加载图片的方法将方法偷偷替换为 imageNamed:inBundle:,但是很遗憾 hook 了 UIImage 所有加载图片的方法,没有一个方法能拿到 XIB 上所设置的图片名,也就意味着我们无法得知优化后的图片在哪个 bundle 下,也就不知道图片该如何加载。虽然有坎坷,但是我们始终坚信 XIB 一定是通过某些方法将图片加载出来的,我们一定能拿到这个过程!为了验证这个问题,首先定义一个 UIImageView 的子类,并将XIB 上的 UIImageView 指定为这个子类。大家都知道通过 XIB 加载的视图都一定会执行 initWithCoder: 方法。

发现在执行 [super initWithCoder:aDecoder] 之前通过 lldb 查看 self.image 是 nil。当执行完这行代码后 self.image 就有值了。因此推断图片的信息(图片名称、路径等信息)都在 aDecoder 中!在网上搜索了一些资料后发现aDecoder 有一些固定的 key,可以通过这些固定的 key 得到一部分信息。如

很显然通过 UIImage 这个 key 能拿到图片,但是很遗憾经过多次尝试没能找到图片的路径信息。因此这个问题的关键是怎么找到合适的 key,为了解决这个问题,最好是能拿到 aDecoder 的解码过程。因此 hook aDecoder 的解码方法 decodeObjectForKey:是个不错的选择。如果能拿到 xib 上设置的图片名称,那么我们就可以根据图片名称获取到正确的图片路径。经过断点查看 aDecoder 是 UINibDecoder(私有类)类型。

- (id)swizzle_decodeObjectForKey:(NSString *)key
{
    Method originalMethod = class_getInstanceMethod([HookTool class], @selector(swizzle_decodeObjectForKey:));
    IMP function = method_getImplementation(originalMethod);
    id (*functionPoint)(id, SEL, id) = (id (*)(id, SEL, id)) function;
    id value = functionPoint(self, _cmd, key);
    
    return value;
}

打印系统 decode 的所有 key 后发现有个 key 为 UIResourceName,value 为图片的名称。也就是说我们能得到 XIB 上设置的图片名称了。但是这个图片的名称怎么传递给这个 XIB 对应的 UIImageView 对象呢?换句话说也就是说我们怎么把图片传给这个 XIB 对应的 view 呢?为了将图片名称传给 UIImageView,需要给 aDecoder 添加一个 block 的关联引用。

在 hook 到的 decodeObjectForKey: 方法中将图片名称回传给 initWithDecoder: 方法。

这里需要注意的是一点是:XIB 默认设置图片是在 rentun value 之后,也就是说如果我们回调过早有可能图片被替换为 nil。因此需要 dispatch_after 一下,等 return 之后再回调图片名称并设置图片。受此启发,我们也可以 hook UIImage 的imageNamed: 方法,根据参数的规则到 xxxCopy.bundle 下获取图片,并返回图片。这就意味着放弃通过脚本修改 API,减少了代码的改动。看到这里似乎是没有什么问题,但是我们忽略了一个很严重的问题 aDecoder 对象和 UIImageView 类型的对象是一一对应的吗?一个 imageView 它的 aDecoder 是它唯一拥有的吗?带着这个问题,我们先来看下打印信息:

重复生成对象并打印后发现 aDecoder 的地址都相同,也就是说存在一个 aDecoder 对应多个 UIImageView 的现象。因此异步方案不适用,需要同步进行设置图片,因此全局变量最为合适。其实这一点很容易理解,aDecoder 是与 XIB 对应的,XIB 是不变的所以 aDecoder 是不变的。因此异步回调的方案不适用,需要同步进行设置图片,在这种情况(主线程串行执行)下跨类传值全局变量最为合适。

- (id)swizzle_decodeObjectForKey:(NSString *)key
{
    Method originalMethod = class_getInstanceMethod([HookTool class], @selector(swizzle_decodeObjectForKey:));
    IMP function = method_getImplementation(originalMethod);
    id (*functionPoint)(id, SEL, id) = (id (*)(id, SEL, id)) function;
    id value = functionPoint(self, _cmd, key);

    NSString* propKey = @"emaNecruoseRIU";
    // 反转字符串
    propKey = [XUtil stringByReversed:propKey];

    if ([key isEqualToString:propKey]) {
        if (normal_imageName) {
            select_imageName = value;
        }
        else {
            normal_imageName = value;
        }
    }
    
    return value;
}

hook UIImageView 的 initWithCoder:

- (id)swizzle_imageView_initWithCoder:(NSCoder *)aDecoder
{
    // 执行顺序:initWithCoder -》DecoderWithKey -》setImage:,所以每次给 imageView 设置图片时,需要将之前的置空。
    // tabbarItem 的图片设置不会执行 initWithCoder,如果不置空,会导致 imageView 设置成和 tabbarItem 一样的图片。
    normal_imageName = nil;
    select_imageName = nil;

    UIImageView * instance = (UIImageView *)[self swizzle_imageView_initWithCoder:aDecoder];

    if (normal_imageName && [normal_imageName isKindOfClass:[NSString class]] && normal_imageName.length > 0) {
        
        UIImage * normalImage = [HookTool imageAfterSearch:normal_imageName];
        // 赋值
        if (normalImage) {
            instance.image = normalImage;
        }
        normal_imageName = nil;
        select_imageName = nil;
    }
    
    return instance;
}

上面两段代码仅仅介绍思路。同理 hook 项目中 UIImage 所用到的加载图片的 API 即可加载图片。如果将所有的 hook 方法放到一个类中,那么只要将这个类拖入到项目中,并将项目中所有的 bundle 下的图片都放到对应的 Assets.xcassets 文件下那么无需修改一行代码即可将所有的图片迁移到 Assets.xcassets 下,达到应用瘦身的目的。

但是我们组内老练的架构师们指出:项目中 hook 如此重要的 API 对增加了项目维护的难度。这也引发了对项目中 AOP 场景的思考,项目中到底 hook 了多少 API?为此特地赶制了一个基于 fishhook 的一个 hook 打印工具,检测和统计项目中的 AOP 情况。但是缺点是必须调整编译顺序保证工具类最先被 load。

hook method_exchangeImplementations 方法。

检测方法(字典写入时不要忘了加锁)。

这种方式不能区分 image 和 backgroundImage、normal 和 Selected。目前根据观察顺序应该是:

UIResourceName : normal - image(前景图)
UIResourceName : normal - backgroundImage(背景图)
UIResourceName : selected - image(前景图)
UIResourceName : selected - backgroundImage(背景图)

Guess you like

Origin www.cnblogs.com/dins/p/ios-nei-zhi-tu-pian-shou-shen.html