Emoji 讓你這麼頭疼,那 EmojiCompat 是如何解決它的?

承香墨影發表於2018-01-04

190

Hi,大家好,我是承香墨影!

今天看題目就知道,繼續來分析 Android 下的 Emoji 。Google 新出的 Support 包裡,增加了一個 EmojiCompat,就是為了解決 Emoji 的問題。

上一篇文章,已經分析瞭如何使用 EmojiCompat 的相關細節,不太瞭解的可以先看看:《Android 開發,你遇上 Emoji 頭疼嗎?》,今天就換一個維度,來從原始碼的角度看看 EmojiCompat 有什麼能讓我們學習的實現細節!

Emoji 確實讓我們頭疼,那我們來分析 Google 給我們的解決方案 EmojiCompat 。不過呢?分析原始碼我們就帶著問題來看它?本文嘗試弄清楚幾個問題?

  1. EmojiCompat 如何保證相容。
  2. EmojiCompat 的實現原理。
  3. 能不能幹掉 BundleEmoji 打入包內的 7.4MB 大小的字型包?

主要是分析 EmojiCompat 的一些細節,如果你對 EmojiCompat 有所瞭解,你會知道它會在 assets 裡,打包的時候,嵌入一個 NotoColorEmojiCompat.ttf 字型檔案,這個字型包大概有 7.4MB ,會直接增加 Apk 的大小,那麼最後我們嘗試解決讓它不直接增加在 Apk 裡。

一、EmojiCompat 基礎結構

EmojiCompat 使用起來,非常的方便,它包含幾個點。在之前的文章中,就已經說清楚了,建議還是先看看之前的文章《Android 開發,你遇上 Emoji 頭疼嗎?》,這裡為了保證文章的完整性,我這裡簡單回顧一下。

EmojiCompat 會使用 ttf 字型,來做到 Emoji 的相容支援,本身在 2010 年之後,Emoji 就已經在 Unicode 中分配了碼點,在那之後,Emoji 就是一個字型。

EmojiCompat 使用的字型包,前面也提過,大概會有 7.4MB,而根據 EmojiCompat 使用的字型檔案的來源,它被分為兩種:

  1. 可下載的字型。
  2. Apk 包內捆綁的字型。

可下載的字型,是 com.android.support:support-emoji:VersionXXX原生支援的,可是它需要依賴 Google 服務,所以在國內大環境下,你基本上是用不上的。

那麼除非你是在做海外產品,否則你只能使用第二種方式,Apk 內捆綁字型,這個時候你就需要使用 com.android.support:support-emoji-bundled:VersionXxx ,它會自動在 Apk 打包的時候,向 assets 目錄下,捆綁嵌入一個 NotoColorEmojiCompat.ttf 字型檔案,它大概有 7.4MB。

這些,操作的其實都是 Emoji 的字串,如果你想更方便的使用 EmojiCompat 還提供了一些 EmojiAppCompatXxx 控制元件,使用它只需要替換專案內需要顯示 Emoji 的 TextView 或者 EditText 等就可以了,不過使用這些支援 Emoji 的控制元件,你需要引用 com.android.support:support-emoji-appcompat:Version 依賴。

好了,到這裡 EmojiCompat 的大致結構就已經清晰了,接下來我們看看 EmojiCompat 的原始碼細節。

二、EmojiCompat 的原始碼細節

2.1 EmojiCompat.Config

EmojiCompat 在使用之前,需要呼叫 EmojiCompat.init() 來進行初始化,而 init() 方法,需要傳遞一個 Config 的抽象類,EmojiCompat 就是根據這個 Config 來決定這個字型檔案的來源,到底是從線上下載還是 Apk 本地捆綁。

先來看 Config 的結構。

EmojiConfig

Config 這個抽象類,提供的幾個抽象方法,作用呢看名字就已經很清晰了。

這裡簡單介紹一下:

1、如果要監聽 EmojiCompat 的初始化狀態,需要使用 registerInitCallback()unregisterInitCallback() ,來增加監聽和解綁監聽。

2、如果要給 Emoji 增加一個背景色,可以使用 setEmojiSpanIndicatorColor() 設定一個背景色,並使用 setEmojiSpanIndicatorEnabled() 將這個開關開啟。

3、設定匹配度,使用 setReplaceAll() 進行配置。

Config 還是很簡單的,這裡說一下 setReplaceAll() 方法,EmojiCompat 預設的模式會優先使用當前裝置自帶的 System Font,在 System Font 不支援這個 Emoji 的時候,才會使用 Support Font 來渲染 Emoji。

emoji-support

之前文章中,給的解決方案是直接將 ttf 字型,使用 Typeface 設定的到 TextView 上去,但是閱讀原始碼之後,發現 EmojiCompat 其實是提供了對應的策略的調整的,就是使用 setReplaceAll() 。當然,使用設定字型的方式也能解決問題,但是總是不及官方提供的方法好,這裡糾錯一下。

2.2 初始化不同字型的策略區分

前面提到了 Config ,而 EmojiCompat 用來區分不同的載入方式,使用的就是不同的 Config 實現類。

這裡就涉及到兩個類:

1、FontRequestEmojiCompatConfig

它可以做到去請求可下載的 Emoji 字型,不過它依賴 Google 服務,在國內進本處於廢棄狀態。

可能這裡簡單看個名字,你會以為它真的是去下載 Emoji 字型,其實並不是。它是依賴 FontProviderHelper 來和系統中的 Google 服務,通過 FileProvider 進行互動,獲取到 Google 服務之前快取好的字型包資源,如果沒有字型的快取,就會由 Google 服務來下載字型。

使用這種方式,非常的優雅,在 Android 的生態下,直接使用系統已經提供好的字型資源,不需要我們再去關心下載的邏輯,可惜大多數情況下我們使用不到,所以這裡不再過多介紹。

2、BundledEmojiCompatConfig

使用 BundledEmojiCompatConfig ,需要提前引入 support-emoji-bundled 依賴。

EmojiBound

新增加的依賴其實也非常的簡單,只有一個有效類,它就是我們需要的 BundledEmojiCompatConfig,接下來我們看看它內部是如何實現的。

BundledEmojiConfig

BundledEmojiCompatConfig 內部實現了 EmojiCompat.MetadataRepoLoader 來做初始化,它其中其實是開了個新執行緒來進行初始化,具體邏輯在 InitRunnable 中。

BundleEmojiRunner

InitRunnable 主要是為了初始化 MetadataRepo ,在此過程中,會去載入前面提到的 NotoColorEmojiCompat.ttf 字型,這是一個耗時操作,被放在子執行緒中去完成。

本質上來說 init() 的過程,實際上就是為了得到的 MetadataRepo 物件,它是用來維護載入好的所有 Emoji 的資料,這裡記住它,之後會用到。

2.3 EmojiCompat 的初始化

前面提到,EmojiCompat 的初始化是需要呼叫它的 init() 方法,而 init() 方法內部其實就是一個最常見的單例,最終還是呼叫的它自己私有的初始化方法。

EmojiCompatMethod

在 EmojiCompat 的構造方法中,會初始化一些必要的資源和從 Config 中提取一些預設配置。

這裡只想需要關注幾個點:

1、EmojiCompat 使用了 ReentrantReadWriteLock,所以大多數操作都是執行緒安全的。

2、EmojiCompat 大部分的實際操作,都是通過 mHelper 來實現的,不同的 Api Level 有不同的實現,也正是通過它,來做到版本相容的。

EmojiCompat 的構造方法,最後一行會呼叫 mHelper.loadMetadata() 方法,Api Level 19 以下使用的 CompatInternal,其實就是空實現,沒有什麼有意義的程式碼,我們這裡主要關注 CompatInternal19 。

loadEmoji

在 CompatInternal19 的 loadMetadata() 中,會去呼叫 MetadataLoader.load() 方法,我們這裡使用本地捆綁的方式載入 Emoji 字型,所以會呼叫到前面介紹的 BundledEmojiCompatConfig 中的 BundledMetadataLoader 去,通過監聽回撥,來獲得初始化的載入的用於存放 Emoji 字型資訊的 MetadatatRepo 類。

2.4 EmojiCompat.process() 的過程

EmojiCompat 替換 Emoji,需要使用它的 process() 方法,當你初始化完成之後,就可以呼叫它了,接下來我們來分析一下 process() 是如何工作的。

process

process() 有多個過載方法,最終都會呼叫到這個引數最多的方法上。我這裡在截圖中隱藏掉了一些不重要的校驗邏輯,不過不影響閱讀。

process() 方法中,可以通過 replaceStrategy 設定 Emoji 字型的替換規則,這裡和 Config 中的設定一樣。而最終,是藉助 EmojiProcessor.process() 來完成操作。

EmojiProcessor.process() 的實現邏輯還是很清晰的,大體上做以下兩個事情。

1、首先判斷傳遞進來的 charSequence 是不是一個 Spannable 內,有沒有 EmojiSpan,有的話全部移除。

2、然後用一個迴圈去檢驗 charSequence 中是否有 Emoji,有的話,使用 EmojiSpan 包裝它。

下面是 EmojiProcessor.process() 的關鍵程式碼。

emojiWhile

當 Action 為 ACTION_FLUSH 的時候,就會使用 EmojiSpan 替換替換它。

2.5 EmojiAppCompatXxx 控制元件的邏輯

如果我們不想直接使用 process() 來操作所有的字串,可以使用 EmojiCompat 提供的一些對 Emoji 支援的控制元件,這就需要引入 support-emoji-appcompat 依賴。

appCompat

在這個包裡,只定義了幾個可能需要顯示字型的控制元件,接下來我們看看它們的實現邏輯,這裡拿 EmojiAppCompatTextView 來研究,其他幾個大致是一樣的。

AppTextView

所有相關的操作都被包裝在了 EmojiTextViewHelper() 中,它需要把當前的 TextView 物件傳遞進去。而在 EmojiTextViewHelper 中,實際上關鍵程式碼是它的 wrapTransformationMethod() 方法,在其中對 TransformationMethod 做了一個包裝,將普通的 TransformationMethod 包裝成了 EmojiTransformationMethod 。

TransformationMethod 有些朋友可能不太清楚他是幹嘛的,簡單來說,它會去替換當前顯示的內容,例如如果你設定一個 TextView 需要顯示 password,輸入的字元會被替換成星號(*),就是它乾的。

這裡使用 EmojiTransformationMethod 對其進行包裝,實際上是想用 Emoji 字型來替換它原本的顯示,關鍵程式碼在 getTransformation() 方法中。

getTrans

可以看到,它實際上也是去呼叫的 EmojiCompat.get().process(),和你直接呼叫並沒有什麼不一樣。

好了,到這裡 EmojiCompat 原始碼的所有相關細節

四、如何不讓ttf 字型打包在 Apk 中

對於 Emoji 的支援,肯定是需要引入一些資源的,這一點是毋庸置疑的。只不過能不能讓這個資源不要被打包在 Apk 裡,我想對於任何 Apk ,一下子增大 7.4MB,都是有壓力的。

那我們來討論一下,如何解決這個問題,我們從線上下載一個 NotoColorEmojiCompat.ttf 字型給 EmojiCompat 可不可以?這樣雖然會增大伺服器下載的流量,但是可以節省 Apk 的體積。能做到這一步,接下來就是一個方案選擇的問題。

從 BundledEmojiCompatConfig 的原始碼中,可以瞭解到,它只是需要一個 ttf 的字型檔案,而如果我們參照它,重寫一個實現 Config 的類,就可以做到從線上下載一個字型檔案,使用這個下載的字型檔案進行初始化。

好了,這樣思路就明確了,那我們看看原始碼細節,看是否能如此實現。

1、Config 是否可被實現。

一個 Config 中,需要使用的 EmojiCompat.Config 和 EmojiCompat.MetadataRepoLoader 都是 public 的,所以可以被開發者自行實現的。

2、MetadataRepo.create() 能不能支援別的來源。

MetadataRepo 是 EmojiCompat 初始化的關鍵,初始化就是為了得到這個物件。而 MetadataRepo.create() 還提供了其他幾個過載方法。

create

第一個引數都需要一個 Typeface ,而 Typeface 是可以載入一個我們指定目錄下的字型檔案的,那就看第二個引數。

第二個引數,是一個 MetadataListReader 物件,它需要的其實就是這個字型檔案的 InputStream。字型檔案的輸入流,檔案都有了,InputStream 一定也能拿到。

到這裡就清晰了,我們完全可以在初始化的時候,從線上下載一個字型檔案,然後再使用 MetadataRepo 去初始化它,最終將狀態返回給 EmojiCompat,來做到我們不將 Emoji 字型打包在 Apk 內的目的。

思路很簡單,當然還需要額外處理一些下載失敗和還沒有初始化完成的時候,如何顯示的問題,這裡就不提供示例程式碼了。

到此,就分析完 EmojiCompat 的所有細節,不知道對你有什麼幫助?

你在做原始碼分析的時候,有什麼技巧?可以在留言中分析給大家!

今天在承香墨影公眾號的後臺,回覆『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、演算法、設計模式、虛擬機器、Linux、Kotlin、Python、爬蟲、Web專案原始碼。

另外還還維護了一個交流群,有興趣可以在公眾號後臺回覆:"加群"

Emoji 讓你這麼頭疼,那 EmojiCompat 是如何解決它的?

我的部落格即將搬運同步至騰訊雲+社群,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan

相關文章