iOS內置圖片瘦身思路整理

来源:http://www.cnblogs.com/dengzhuli/archive/2017/11/10/7814696.html
-Advertisement-
Play Games

一、前言 前段時間註意到我們APP的包大小超過100MB了,所以隨口跟老闆說了下能否採用字體文件(.ttf)替代PNG圖片,老闆對應用瘦身很感興趣因此讓我做下技術調研。這篇文章主要是將我們的各個技術方案的思路做一下整理和總結,希望對大家有所幫助。 二、iOS內置資源的集中方式 在介紹技術方案前我們先 ...


一、前言

前段時間註意到我們APP的包大小超過100MB了,所以隨口跟老闆說了下能否採用字體文件(.ttf)替代PNG圖片,老闆對應用瘦身很感興趣因此讓我做下技術調研。這篇文章主要是將我們的各個技術方案的思路做一下整理和總結,希望對大家有所幫助。

二、iOS內置資源的集中方式

在介紹技術方案前我們先來看下iOS內置圖片資源都有哪些常見的方式:

1、將圖片存放在bundle下

這是一種非常常見的方式,項目中各類文件分類放在各個bundle下,項目既整潔又能達到隔離資源的目的。我們項目中圖片絕大多數都是這樣內置的,其載入方式為[UIImage imageNamed:"xxx.bundle/xxx.png"](請記住這個字元串的規則,因為這種規則非常非常重要!!!"xxx.bundle/xxx.png")。但是這種方式有比較明顯的缺點:首先使用bundle存儲圖片iOS系統不會對其進行壓縮存儲,造成了應用體積的增大。其次是使用bundle存儲圖片放棄了APP thinning,其明顯的表現是使用2倍屏手機的用戶和使用3倍屏手機的用戶下載的應用包大小一樣。如果能夠實現APP thinning,那麼往往2倍屏幕的手機包大小會小於3倍屏手機的包大小,起到差異性優化的目的。在調研過程中我們還發現,應用的體積與圖片資源的數量密切相關(聽起來好像是廢話)。換句話說,iPhone的rom存在4K對齊的情況,一張498B大小的圖片在應用包中也要占據4KB大小。因此項目中每添加一張圖片就至少增大了4KB。為了證實這個觀點特地創建空應用進行測試。首先創建空應用,其大小在7P上為213KB,引入一張498B的圖片前後對比如下:


一張498B的圖片

 


占據4KB磁碟空間
未添加資源的應用
添加圖片資源後的大小

上述實驗未經過App Store上線認證,僅僅通過本地打包測試,因此觀點僅供參考。

2、使用.ttf字體文件替代圖標

使用字體文件替代圖片也是一種比較常見的資源內置方式。很多應用都使用過這種方案,如淘寶、愛奇藝等知名應用,都採用過這種方式。使用字體文件的好處是顯而易見的,如果APP中某個圖片比較大,那麼為了保證清晰度,UI可能會提供比較大的圖標。使用字體文件會避免這個問題,而且不必導入@2x和@3x圖片,一套字體文件就能保證UI的清晰度。關於如何生成.ttf文件在這裡就不在贅述了(因為我並不喜歡這個方案),我們只要如何使用就可以了。字體文件使用起來比較簡單,但是使用方法與png圖片的使用方法有很大的不同,因為字體文件時機所展示的圖標都是UTF8編碼轉來的字元串。因此當我們需要展示一個圖標的時候不再是使用UIImageView了,而是UILabel。


字體文件展示圖片的代碼示例

由於我們使用了字體來替代圖片,所以我們可以通過設置字體的顏色來改變圖標的顏色。我們之前經常會遇到一個場景,如兩個一模一樣的圖標但是由於顏色不同,UI同學就需要提供2套圖片,每套圖片中包含@2x和@3x圖片。如果採用了字體替代簡單的圖標,那麼UI只需要提供一套字體即可,並且拉伸後也不會失真。使用字體文件的好處總結起來主要有兩點:

1、可以降低應用圖片內置資源的體積。

2、可以隨意放縮和修改顏色。

但是其缺點也很明顯:

1、圖標的查找和替換比較麻煩,不如直接使用圖片那樣簡單。

2、最重要的是如果在58同城APP中使用,則意味著無法替換之前存在的圖片,只能起到縮小增量的目的,無法減小全量。

ps:任何一種需要大刀闊斧改革的優化都是一種不明智的行為。

3、圖片存在Assets.xcassets下(蘋果推薦,我也推薦)

使用Assets.xcassets是蘋果推薦的一種方式。Assets.xcassets是iOS7推出的一種圖片資源管理工具,將圖片內置到Assets.xcassets下系統會對圖片資源進行壓縮,並且支持APP thinning。


APP Slicing

項目優化不能脫離場景,很多很好的方案由於場景的限制並不能起到優化的作用。因此先簡單介紹下我們的項目場景:為了達到跨團隊快速開發的目的,我們項目很早就利用cocoapods實現組件化。項目中存在多個業務pod,每個pod都有各自的團隊維護,各個團隊的代碼彼此不開放,各個pod最終會被編譯為.a的形式。這裡需要說明一下我為什麼要強調.a,與.a相對應的還有一個.framework,他們之間有一個重要的區別就是資源的問題。framework中可以存放資源,但是.a卻不可以,因此生成.a的pod下的資源會被轉移到main bundle下,這為資源衝突造成了隱患,為了避免這種衝突我們之前採用的使用bundle管理資源,bundle名很少會重覆這樣就大大降低了資源衝突的可能性。優化的前提之一也是不破壞這種組件化開發的模式,換句話說也就是各個業務線不產生資源耦合、業務線的RD不必擔心彼此資源的衝突、業務Pod下的資源文件彼此隔離。哪怕招聘團隊中存在a.png,房產團隊中也存在a.png也不會有什麼問題。所以我們先要拋出兩個問題:

1、cocoapods是否支持使用Assets.xcassets。

2、各個pod各自維護自己的Assets.xcassets會不會造成資源衝突。

為了弄清楚上面兩個問題,我們先要看下podspec的幾個重要參數:


podspec

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

source_files :源文件路徑。

s.resources :資源文件路徑及文件類型。

s.resource_bundles :資源文件路徑及類型,同時資源文件會被打成bundle。(推薦使用)。

實驗發現各個pod下都可以創建自己的xcassets,因此問題1不算問題是問題。如果我們在各個業務pod下都創建.xcassets文件內置圖片,那麼cocoapods的腳本會在編譯時將各個目錄下的xcassets文件內容提取出來,合併到一個xcassets中並生成一個.car文件。這樣的話如果資源文件重名,那麼很可能其中某一個文件會被覆蓋替換。因此我們主要是要解決問題2。查看podspec的寫法發現s.resource_bundles貌似是我們所需要的法寶。為此我們天真的以為問題馬上就要解決了:


將指定路徑下的資源打包成bundle

最終打包結果很理想,確實能夠生成ImagesBundle.bundle,並且bundle下存在Assets.car。


mainbundle下存在ImagesBundle
ImageBundle.bundle下存在Assets.car

事情到這裡可能已經看到曙光了,但是我們發現通過

[UIImage imageNamed:@"ImagesBundle.bundle/1"];

載入不出來圖片。必須使用

[UIImageimageNamed:@"1"inBundle:[WBIMViewControllericonBundle]compatibleWithTraitCollection:nil];

才能載入出來。

 


圖片載入失敗
指定bundle後載入成功

也就是說只有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:,從符合xxx.bundle/yyy.png特征的參數中獲取bundle名和圖片名xxx_Asset.bundle和yyy.png,獲取圖片並返回。

4. 查找並全部替換imageNamed: 和 imageWithContentOfFile:為imageWithName:

只要能拿到原來代碼中imageNamed:的參數就能知道現在圖片存在那個bundle下,這樣就能通過imageNamed:inBundle:獲取到圖片,其思路如下圖所示:


imageWithName:方法內部處理
打包後bundle情況

看到這裡老司機們已經應該能遇見這種優化的成本了。載入圖片都需要指定bundle也就意味著成千上萬處的API需要修改。我們最初探討到這裡的時候首先想到的是腳本,但是這個方案很快就被否定了,因為項目中存在大量的XIB,XIB中設置圖片我們無法通過腳本替換API。

為瞭解決XIB設置圖片的問題,我們首先想到了AOP。通過hook XIb載入圖片的方法將方法偷偷替換為imageNamed:inBundle:  ,但是很遺憾我們hook了UIImage所有載入圖片的方法,沒有一個方法能拿到XIB上所設置的圖片名稱,也就意味著我們無法得知優化後的圖片在哪個bundle下,也就不知道圖片該如何載入。雖然有坎坷,但是我們始終堅信XIB一定是通過某些方法將圖片載入出來的,我們一定能拿到這個過程!為了驗證這個問題,首先定義一個UIImageView 的子類,並將XIB上的UIImageView指定為這個子類。大家都知道通過XIB載入的視圖都一定會執行initWithCoder:方法


UIImageView的子類載入

我們發現在得到執行[super initWithCoder:aDecoder]之前通過lldb查看slef.image是nil。當執行完這行代碼後self.image就有值了。因此推斷圖片的信息(圖片名稱、路徑等信息)都在aDecoder中!在網上搜索了一些資料後發現aDecoder有一些固定的key,可以通過這些固定的key得到一部分信息。如


aDecoder可以通過某些key得到其中信息

很顯然通過“UIImage”這個key能拿到圖片,但是很遺憾經過多次嘗試沒能找到圖片的路徑信息。因此這個問題的關鍵是怎麼找到合適的key,為瞭解決這個問題,最好是能拿到aDecoder的解碼過程。因此hook aDecoder的解碼方法decodeObjectForKey:是個不錯的選擇。如果能拿到xib上設置的圖片名稱那麼我們就可以根據圖片名稱獲取到正確的圖片路徑。經過斷點查看aDecoder 是UINibDecoder(私有類)類型。


aDecoder
hook UINibDecoder的decode方法

列印系統decode的所有key 後發現有個key為UIResourceName,value為圖片的名稱。也就是說我們能得到XIB上設置的圖片名稱了。但是這個圖片的名稱怎麼傳遞給這個XIB對應的UIImageView 對象呢?換句話說也就是說我們怎麼把圖片傳給這個XIB對應的view呢?為了將圖片名稱傳給UIImageView,需要給aDecoder添加一個block的關聯引用。


UIImageView在initWithCoder:的時候設置回調

在hook到的decodeObjectForKey:方法中將圖片名稱回傳給initWithDecoder:方法:


aDecoder hook到圖片名稱後回調給UIImageView類

這裡需要註意的是一點是:XIB 預設設置圖片是在rentun value之後,也就是說如果我們回調過早有可能圖片被替換為nil。因此需要dispatch_after一下,等return 之後再回調圖片名稱並設置圖片。受此啟發,我們也可以hook UIImage 的imageNamed:方法,根據參數的規則到xxxCopy.bundle下獲取圖片,並返回圖片。這就意味著放棄通過腳本修改API,減少了代碼的改動。看到這裡似乎是沒有什麼問題,但是我們忽略了一個很嚴重的問題aDecoder對象和UIImageView類型的對象是一一對應的嗎?一個imageView它的aDecoder是它唯一擁有的嗎?帶著這個問題,我們先來看下列印信息:


重覆生成UIImageView對象和aDecoder對照關係

重覆生成對象並列印後發現aDecoder的地址都相同,也就是說存在一個aDecoder對應多個UIImageView的現象。因此非同步方案不適用,需要同步進行設置圖片,因此全局變數最為合適。其實這一點很容易理解,aDecoder是與XIB對應的,XIB是不變的所以aDecoder是不變的。因此非同步回調的方案不適用,需要同步進行設置圖片,在這種情況(主線程串列執行)下跨類傳值全局變數最為合適:

 


hook UINibDecoder的decodeObjectForKey                                           
hook UIImageView 的initWithCoder:

上面兩段代碼僅僅介紹思路,可能載入圖片的代碼並不是十分的嚴謹,請讀者自己鑒別。同理hook 項目中UIImage 所用到的載入圖片的API即可載入圖片。如果將所有的hook方法放到一個類中,那麼只要將這個類拖入到項目中,並將項目中所有的bundle下的圖片都放到對應的Assets.xcassets文件下那麼無需修改一行代碼即可將所有的圖片遷移到Assets.xcassets下,達到應用瘦身的目的。但是我們組內老練的架構師們指出:項目中hook如此重要的API對增加了項目維護的難度。這也引發了我對項目中AOP場景的思考,項目中到底hook 了多少API?可能在我場多年的老司機們都難以回答了,為此特地趕製了一個基於fishhook的一個hook列印工具,檢測和統計項目中的AOP情況。但是缺點是必須調整編譯順序保證工具類最先被load。


hook method_exchangeImplementations 方法 
檢測方法(字典寫入時不要忘了加鎖)

 

 

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Set是無重覆值的有序列表。Set會自動移除重覆的值,因此你可以使用它來過濾數組中重覆的值並返回結果。 Map是有序的鍵值對,其中的鍵允許是任何類型。 Set和Map是es6新增的兩個數據集合。 Set集合 es6新增了set類型,這是一種無重覆值的有序列表。Set允許對它包含的數據進行快速訪問。 ...
  • 1、技術點 移動端自適應採用百分比佈局比較適合。需要說明一點的是:height的百分比是以父元素的寬度計算的,由於移動端父元素寬度有時沒有給定值(如父元素寬度為100%),此時子元素的height就為0。 因此子元素的高度值使用padding-top或者padding-bottom撐起。 同時hei ...
  • web前端怎麼樣才能入門,首先我們要從什麼是初級web前端工程師說起: 按照我的想法,我把前端工程師分為了入門、初級、中級、高級這四個級別: 入門級別指的是瞭解什麼是前端(前端到底是什麼其實很多人還是不清楚的),瞭解基本的html、css和javascript語法(這些語方面的東西網上隨便搜一下就有 ...
  • 1、通過原生js獲取this對象 ``` <!DOCTYPE html><html> <head> <meta charset="utf-8" /> <title></title> </head> <body> <form action="" class="files" > <label class ...
  • 通過上一節課創建了一個Android的Hello World項目,項目預設包含一系列源文件,它讓我們可以立即運行應用程式。 如何運行Android應用取決於兩件事情:是否有一個Android設備和是否正在使用Android Studio開發程式。本節課將會教使用Android Studio和命令行兩 ...
  • 先看效果看 載入了一張image,根據四個頂點任意變換。 知識點:1.BitmapContext 2.矩陣變換 一.什麼是BitmapContext 官方解釋: The number of components for each pixel in a bitmap graphics context ...
  • 如下分析針對的API 25的AsyncTask的源碼: 使用AsyncTask如果是調用execute方法則是同步執行任務,想要非同步執行任務可以直接調用executeOnExecutor方法,多數情況下我們會使用AsyncTask內部靜態的線程池, THREAD_POOL_EXECUTOR,這裡並不 ...
  • 轉載請標明出處:http://blog.csdn.net/zhaoyanjun6/article/details/78112856 本文出自 "【趙彥軍的博客】" 在插件開發過程中,我們按照開發一個正式的項目來操作,需要整理一些常用工具類。 Http 請求封裝 在插件的項目中,我們看到依賴庫如下圖所 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...