Android性能優(yōu)化之弱網(wǎng)優(yōu)化詳解
弱網(wǎng)優(yōu)化
匯總了一下眾多大佬的性能優(yōu)化文章,知識點,主要包含:
UI優(yōu)化/啟動優(yōu)化/崩潰優(yōu)化/卡頓優(yōu)化/安全性優(yōu)化/弱網(wǎng)優(yōu)化/APP深度優(yōu)化等等等~
本篇是第五篇:弱網(wǎng)優(yōu)化 [非商業(yè)用途,如有侵權,請告知我,我會刪除]
強調(diào)一下: 性能優(yōu)化的開發(fā)文檔跟之前的面試文檔一樣,需要的跟作者直接要。
1、Serializable原理
通常我們使用Java的序列化與反序列化時,只需要將類實現(xiàn)Serializable
接口即可,剩下的事情就交給了jdk。今天我們就來探究一下,Java序列化是怎么實現(xiàn)的,然后探討一下幾個常見的集合類,他們是如何處理序列化帶來的問題的。
1.1 分析過程
幾個待思考的問題
- 為什么序列化一個對象時,僅需要實現(xiàn)
Serializable
接口就可以了。 - 通常我們序列化一個類時,為什么推薦的做法是要實現(xiàn)一個靜態(tài)
final
成員變量serialVersionUID
。 - 序列化機制是怎么忽略
transient
關鍵字的, static變量也不會被序列化。
接下來我們就帶著問題,在源碼中找尋答案吧。
1.2 Serializable接口
先看Serializable
接口,源碼很簡單,一個空的接口,沒有方法也沒有成員變量。但是注釋非常詳細,很清楚的描述了Serializable
怎么用、能做什么,很值得一看,我們撿幾個重點的翻譯一下,
/** * Serializability of a class is enabled by the class implementing the * java.io.Serializable interface. Classes that do not implement this * interface will not have any of their state serialized or * deserialized. All subtypes of a serializable class are themselves * serializable. The serialization interface has no methods or fields * and serves only to identify the semantics of being serializable. */
類的可序列化性通過實現(xiàn)java.io.Serializable
接口開啟。未實現(xiàn)序列化接口的類不能序列化,所有實現(xiàn)了序列化的子類都可以被序列化。Serializable
接口沒有方法和屬性,只是一個識別類可被序列化的標志。
/** * Classes that require special handling during the serialization and * deserialization process must implement special methods with these exact * signatures: * * <PRE> * private void writeObject(java.io.ObjectOutputStream out) * throws IOException * private void readObject(java.io.ObjectInputStream in) * throws IOException, ClassNotFoundException; * private void readObjectNoData() * throws ObjectStreamException; * </PRE> */
在序列化過程中,如果類想要做一些特殊處理,可以通過實現(xiàn)以下方法writeObject()
, readObject()
, readObjectNoData()
,其中,
- writeObject方法負責為其特定類寫入對象的狀態(tài),以便相應的
readObject()
方法可以還原它。 readObject()
方法負責從流中讀取并恢復類字段。- 如果某個超類不支持序列化,但又不希望使用默認值怎么辦?
writeReplace()
方法可以使對象被寫入流之前,用一個對象來替換自己。 readResolve()
通常在單例模式中使用,對象從流中被讀出時,可以用一個對象替換另一個對象。
1.3 ObjectOutputStream
//我們要序列化對象的方法實現(xiàn)一般都是在這個函數(shù)中 public final void writeObject(Object obj) throws IOException { ... try { //寫入的具體實現(xiàn)方法 writeObject0(obj, false); } catch (IOException ex) { ... throw ex; } } private void writeObject0(Object obj, boolean unshared) throws IOException { ...省略 Object orig = obj; Class<?> cl = obj.getClass(); ObjectStreamClass desc; for (;;) { // REMIND: skip this check for strings/arrays? Class<?> repCl; //獲取到ObjectStreamClass,這個類很重要 //在它的構造函數(shù)初始化時會調(diào)用獲取類屬性的函數(shù) //最終會調(diào)用getDefaultSerialFields這個方法 //在其中通過flag過濾掉類的某一個為transient或static的屬性(解釋了問題3) desc = ObjectStreamClass.lookup(cl, true); if (!desc.hasWriteReplaceMethod() || (obj = desc.invokeWriteReplace(obj)) == null || (repCl = obj.getClass()) == cl) { break; } cl = repCl; } //其中主要的寫入邏輯如下 //String, Array, Enum本身處理了序列化 if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); //重點在這里,通過`instanceof`判斷對象是否為`Serializable` //這也就是普通自己定義的類如果沒有實現(xiàn)`Serializable` //在序列化的時候會拋出異常的原因(解釋了問題1) } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } } ... } private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared) throws IOException { ... try { desc.checkSerialize(); //寫入二進制文件,普通對象開頭的魔數(shù)0x73 bout.writeByte(TC_OBJECT); //寫入對應的類的描述符,見底下源碼 writeClassDesc(desc, false); handles.assign(unshared ? null : obj); if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } private void writeClassDesc(ObjectStreamClass desc, boolean unshared) throws IOException { //句柄 int handle; //null描述 if (desc == null) { writeNull(); //類對象引用句柄 //如果流中已經(jīng)存在句柄,則直接拿來用,提高序列化效率 } else if (!unshared && (handle = handles.lookup(desc)) != -1) { writeHandle(handle); //動態(tài)代理類描述符 } else if (desc.isProxy()) { writeProxyDesc(desc, unshared); //普通類描述符 } else { //該方法會調(diào)用desc.writeNonProxy(this)如下 writeNonProxyDesc(desc, unshared); } } void writeNonProxy(ObjectOutputStream out) throws IOException { out.writeUTF(name); //寫入serialVersionUID out.writeLong(getSerialVersionUID()); ... } public long getSerialVersionUID() { // 如果沒有定義serialVersionUID // 序列化機制就會調(diào)用一個函數(shù)根據(jù)類內(nèi)部的屬性等計算出一個hash值 // 這也是為什么不推薦序列化的時候不自己定義serialVersionUID的原因 // 因為這個hash值是根據(jù)類的變化而變化的 // 如果你新增了一個屬性,那么之前那些被序列化后的二進制文件將不能反序列化回來,Java會拋出異常 // (解釋了問題2) if (suid == null) { suid = AccessController.doPrivileged( new PrivilegedAction<Long>() { public Long run() { return computeDefaultSUID(cl); } } ); } //已經(jīng)定義了SerialVersionUID,直接獲取 return suid.longValue(); } //分析到這里,要插一個我對序列化后二進制文件的一點個人見解,見下面
1.4 序列化后二進制文件的一點解讀
如果我們要序列化一個List<PhoneItem>
, 其中PhoneItem
如下,
class PhoneItem implements Serializable { String phoneNumber; }
構造List的代碼省略,假設我們序列化了一個size
為5的List
,查看二進制文件大概如下所示,
7372 xxxx xxxx 7371 xxxx xxxx 7371 xxxx xxxx 7371 xxxx xxxx 7371 xxxx xxxx
通過剛才的源碼解讀,開頭的魔數(shù)0x73表示普通對象,72表示類的描述符號,71表示類描述符為引用類型。管中窺豹可知一點薄見,在解析二進制文件的時候,就是通過匹配魔數(shù) (magic number) 開頭方式,從而轉(zhuǎn)換成Java對象的。當在序列化過程中,如果流中已經(jīng)有同樣的對象,那么之后的序列化可以直接獲取該類對象句柄,變?yōu)橐妙愋停瑥亩岣咝蛄谢省?/p>
//通過writeSerialData調(diào)用走到真正解析類的方法中,有沒有復寫writeObject處理的邏輯不太一樣 //這里以默認沒有復寫writeObject為例,最后會調(diào)用defaultWriteFields方法 private void defaultWriteFields(Object obj, ObjectStreamClass desc) throws IOException { ... int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte[primDataSize]; } desc.getPrimFieldValues(obj, primVals); //寫入屬性大小 bout.write(primVals, 0, primDataSize, false); ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; desc.getObjFieldValues(obj, objVals); for (int i = 0; i < objVals.length; i++) { ... try { //遍歷寫入屬性類型和屬性大小 writeObject0(objVals[i], fields[numPrimFields + i].isUnshared()); } finally { if (extendedDebugInfo) { debugInfoStack.pop(); } } } }
由于反序列化過程和序列化過程類似,這里不再贅述。
1.5 常見的集合類的序列化問題
1.5.1 HashMap
Java要求被反序列化后的對象要與被序列化之前的對象保持一致,但因為hashmap的key是通過hash計算的。反序列化后計算得到的值可能不一致(反序列化在不同的jvm環(huán)境下執(zhí)行)。所以HashMap需要重寫序列化實現(xiàn)的過程,避免出現(xiàn)這種不一致的情況。
具體操作是將要自定義處理的屬性定義為transient
,然后復寫writeObject
,在其中做特殊處理
private void writeObject(java.io.ObjectOutputStream s) throws IOException { int buckets = capacity(); // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); //寫入hash桶的容量 s.writeInt(buckets); //寫入k-v的大小 s.writeInt(size); //遍歷寫入不為空的k-v internalWriteEntries(s); }
1.5.2 ArrayList
因為在ArrayList中的數(shù)組容量基本上都會比實際的元素的數(shù)大, 為了避免序列化沒有元素的數(shù)組而重寫writeObject
和readObject
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ ... s.defaultWriteObject(); // 寫入arraylist當前的大小 s.writeInt(size); // 按照相同順序?qū)懭朐? for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } ... }
2、Parcelable
Parcelable是Android為我們提供的序列化的接口,Parcelable相對于Serializable的使用相對復雜一些,但Parcelable的效率相對Serializable也高很多,這一直是Google工程師引以為傲的,有時間的可以看一下Parcelable和Serializable的效率對比號稱快10倍的效率
Parcelable接口的實現(xiàn)類是可以通過Parcel寫入和恢復數(shù)據(jù)的,并且必須要有一個非空的靜態(tài)變量 CREATOR, 而且還給了一個例子,這樣我們寫起來就比較簡單了,但是簡單的使用并不是我們的最終目的
通過查看Android源碼中Parcelable可以看出,Parcelable實現(xiàn)過程主要分為序列化,反序列化,描述三個過程,下面分別介紹下這三個過程
2.1 Parcel的簡介
在介紹之前我們需要先了解Parcel是什么?Parcel翻譯過來是打包的意思,其實就是包裝了我們需要傳輸?shù)臄?shù)據(jù),然后在Binder中傳輸,也就是用于跨進程傳輸數(shù)據(jù)
簡單來說,Parcel提供了一套機制,可以將序列化之后的數(shù)據(jù)寫入到一個共享內(nèi)存中,其他進程通過Parcel可以從這塊共享內(nèi)存中讀出字節(jié)流,并反序列化成對象,下圖是這個過程的模型。
Parcel可以包含原始數(shù)據(jù)類型(用各種對應的方法寫入,比如writeInt(),writeFloat()等),可以包含Parcelable對象,它還包含了一個活動的IBinder對象的引用,這個引用導致另一端接收到一個指向這個IBinder的代理IBinder。
Parcelable通過Parcel實現(xiàn)了read和write的方法,從而實現(xiàn)序列化和反序列化??梢钥闯霭烁鞣N各樣的read和write方法,最終都是通過native方法實現(xiàn)
2.2 Parcelable的三大過程介紹(序列化、反序列化、描述)
到這里,基本上關系都理清了,也明白簡單的介紹和原理了,接下來在實現(xiàn)Parcelable之前,介紹下實現(xiàn)Parcelable的三大流程
首先寫一個類實現(xiàn)Parcelable接口,會讓我們實現(xiàn)兩個方法
2.2.1 描述
其中describeContents就是負責文件描述,首先看一下源碼的解讀
通過上面的描述可以看出,只針對一些特殊的需要描述信息的對象,需要返回1,其他情況返回0就可以
2.2.2 序列化
我們通過writeToParcel方法實現(xiàn)序列化,writeToParcel返回了Parcel,所以我們可以直接調(diào)用Parcel中的write方法,基本的write方法都有,對象和集合比較特殊下面單獨講,基本的數(shù)據(jù)類型除了boolean其他都有,Boolean可以使用int或byte存儲
2.2.3 反序列化
反序列化需要定義一個CREATOR的變量,上面也說了具體的做法,這里可以直接復制Android給的例子中的,也可以自己定義一個(名字千萬不能改),通過匿名內(nèi)部類實現(xiàn)Parcelable中的Creator的接口
2.3 Parcelable的實現(xiàn)和使用
根據(jù)上面三個過程的介紹,Parcelable就寫完了,就可以直接在Intent中傳輸了,可以自己寫兩個Activity傳輸一下數(shù)據(jù)試一下,其中一個putExtra另一個getParcelableExtra即可
這里實現(xiàn)Parcelable也很簡單:
1.寫一個類實現(xiàn)Parcelable然后alt+enter 添加Parcelable所需的代碼塊,AndroidStudio會自動幫我們實現(xiàn)(這里需要注意如果其中包含對象或集合需要把對象也實現(xiàn)Parcelable)
2.4 Parcelable中對象和集合的處理
如果實現(xiàn)Parcelable接口的對象中包含對象或者集合,那么其中的對象也要實現(xiàn)Parcelable接口
寫入和讀取集合有兩種方式:
一種是寫入類的相關信息,然后通過類加載器去讀取, –> writeList | readList
二是不用類相關信息,創(chuàng)建時傳入相關類的CREATOR來創(chuàng)建 –> writeTypeList | readTypeList | createTypedArrayList
第二種效率高一些,但一定要注意如果有集合定義的時候一定要初始化
like this –>
public ArrayList authors = new ArrayList<>();
2.5 Parcelable和Serializable的區(qū)別和比較
Parcelable和Serializable都是實現(xiàn)序列化并且都可以用于Intent間傳遞數(shù)據(jù),Serializable是Java的實現(xiàn)方式,可能會頻繁的IO操作,所以消耗比較大,但是實現(xiàn)方式簡單 Parcelable是Android提供的方式,效率比較高,但是實現(xiàn)起來復雜一些 , 二者的選取規(guī)則是:內(nèi)存序列化上選擇Parcelable, 存儲到設備或者網(wǎng)絡傳輸上選擇Serializable(當然Parcelable也可以但是稍顯復雜)
3、http與https原理詳解
3.1 概述
早期以信息發(fā)布為主的Web 1.0時代,HTTP已可以滿足絕大部分需要。證書費用、服務器的計算資源都比較昂貴,作為HTTP安全擴展的HTTPS,通常只應用在登錄、交易等少數(shù)環(huán)境中。但隨著越來越多的重要業(yè)務往線上轉(zhuǎn)移,網(wǎng)站對用戶隱私和安全性也越來越重視。對于防止惡意監(jiān)聽、中間人攻擊、惡意劫持篡改,HTTPS是目前較為可行的方案,全站HTTPS逐漸成為主流網(wǎng)站的選擇。
3.2 HTTP簡介
HTTP(HyperText Transfer Protocol,超文本傳輸協(xié)議),是一種無狀態(tài) (stateless)
協(xié)議,提供了一組規(guī)則和標準,從而讓信息能夠在互聯(lián)網(wǎng)上進行傳播。在HTTP中,客戶端通過Socket創(chuàng)建一個TCP/IP連接,并連接到服務器,完成信息交換后,就會關閉TCP連接。(后來通過Connection: Keep-Alive
實現(xiàn)長連接)
3.2.1 HTTP消息組成
- 請求行或響應行
- HTTP頭部
- HTML實體,包括請求實體和響應實體 HTTP請求結(jié)構,響應結(jié)構如圖所示:
HTTP頭部
HTTP頭部由一系列的鍵值對組成,允許客戶端向服務器端發(fā)送一些附加信息或者客戶端自身的信息,如:accept、accept-encoding、cookie
等。http頭后面必須有一個空行
請求行
請求行由方法、URL、HTTP版本組成。
響應行
響應行由HTTP版本、狀態(tài)碼、信息提示符組成。
3.3 HTTP2.0和HTTP1.X相比的新特性
- 新的二進制格式,HTTP1.x的解析是基于文本?;谖谋緟f(xié)議的格式解析存在天然缺陷,文本的表現(xiàn)形式有多樣性,要做到健壯性考慮的場景必然很多,二進制則不同,只認0和1的組合?;谶@種考慮HTTP2.0的協(xié)議解析決定采用二進制格式,實現(xiàn)方便且健壯。
- 多路復用,即每一個請求都是是用作連接共享機制的。一個請求對應一個id,這樣一個連接上可以有多個請求,每個連接的請求可以隨機的混雜在一起,接收方可以根據(jù)請求的
id
將請求再歸屬到各自不同的服務端請求里面。 - header壓縮,HTTP1.x的
header
帶有大量信息,而且每次都要重復發(fā)送,HTTP2.0使用encoder來減少需要傳輸?shù)膆eader大小,通訊雙方各自cache一份header fields
表,既避免了重復header的傳輸,又減小了需要傳輸?shù)拇笮 ?/li> - 服務端推送,同
SPDY
一樣,HTTP2.0也具有server push
功能。
3.4 HTTP安全問題
- 通信使用明文(不加密),內(nèi)容可能會被竊聽
- 不驗證通信方的身份,因此有可能遭遇偽裝
- 無法證明報文的完整性,所以有可能已遭篡改
3.5 HTTPS協(xié)議
HTTPS 是最流行的 HTTP 安全形式。使用 HTTPS 時,所有的 HTTP 請求和響應數(shù)據(jù)在發(fā)送到網(wǎng)絡之前,都要進行加密。 HTTPS 在 HTTP 下面提供了一個傳輸級的密碼安全層——可以使用 SSL,也可以使用其后繼者——傳輸層安全(TLS)。
相關術語
- 密鑰:改變密碼行為的數(shù)字化參數(shù)。
- 對稱密鑰加密系統(tǒng):編、解碼使用相同密鑰的算法。
- 不對稱密鑰加密系統(tǒng):編、解碼使用不同密鑰的算法。
- 公開密鑰加密系統(tǒng):一種能夠使數(shù)百萬計算機便捷地發(fā)送機密報文的系統(tǒng)。
- 數(shù)字簽名:用來驗證報文未被偽造或篡改的校驗和。
- 數(shù)字證書:由一個可信的組織驗證和簽發(fā)的識別信息。
- 密鑰協(xié)商算法:解決動態(tài)密鑰分配、存儲、傳輸?shù)葐栴}
3.5.1 TLS/SSL協(xié)議
TLS/SSL協(xié)議包含以下一些關鍵步驟:
- 傳輸?shù)臄?shù)據(jù)必須具有
機密性
和完整性
,一般采用對稱加密算法
和HMAC算法
,這兩個算法需要一系列的密鑰塊(key_block)
,比如對稱加密算法的密鑰、HMAC算法的密鑰,如果是AES-128-CBC-PKCS#7加密標準,還需要初始化向量。 - 所有的加密塊都由
主密鑰(Master Secret)
生成,主密鑰就是第1章中講解的會話密鑰,使用密碼衍生算法將主密鑰轉(zhuǎn)換為多個密碼快。 - 主密鑰來自
預備主密鑰(Premaster Secret)
,預備主密鑰采用同樣的密碼衍生算法轉(zhuǎn)換為主密鑰,預備主密鑰采用RSA或者DH(ECDH)算法協(xié)商而來。不管采用哪種密鑰協(xié)商算法,服務器必須有一對密鑰(可以是RSA或者ECDSA密鑰),公鑰發(fā)給客戶端,私鑰自己保留。不同的密鑰協(xié)商算法,服務器密鑰對的作用也是不同的。 - 通過這些關鍵步驟,好像TLS/SSL協(xié)議的任務已經(jīng)結(jié)束,但這種方案會遇到中間人攻擊,這是TLS/SSL協(xié)議無法解決的問題,必須結(jié)合
PKI
的技術進行解決,PKI的核心是證書,證書背后的密碼學算法是數(shù)字簽名
技術。對于客戶端來說,需要校驗證書,確保接收到的服務器公鑰是經(jīng)過認證的,不存在偽造,也就是客戶端需要對服務器的身份進行驗證。
TLS/SSL協(xié)議核心就三大步驟:認證、密鑰協(xié)商、數(shù)據(jù)加密
。
3.5.2 RSA握手
握手階段分成五步:
- 客戶端給出協(xié)議版本號、生成的
隨機數(shù)(Client random)
,以及客戶端支持的加密方法。 - 服務端確認雙方使用的加密方法,并給出數(shù)字證書、以及一個服務器生成的隨機數(shù)。
- 客戶端確認數(shù)字證書有效,然后生成一個新的
隨機數(shù)(Premaster secret)
,并使用數(shù)字證書中的公鑰,加密這個隨機數(shù),發(fā)給服務器。 - 服務器使用自己的私鑰,獲取客戶端發(fā)來的
隨機數(shù)(Premaster secret)
。 - 雙方根據(jù)約定的加密方法,使用前面的三個隨機數(shù),生成
會話密鑰(session key)
,用來加密接下來的對話過程。
握手階段有三點需要注意:
- 生成對話密鑰一共需要
三個隨機數(shù)
。 - 握手之后的對話使用
對話密鑰(session key)
加密(對稱加密),服務器的公鑰
和私鑰
只用于加密和解密對話密鑰(session key)
(非對稱加密),無其他作用。 - 服務器
公鑰
放在服務器的數(shù)字證書
之中。
3.5.3 DH握手
RSA整個握手階段都不加密,都是明文的。因此,如果有人竊聽通信,他可以知道雙方選擇的加密方法,以及三個隨機數(shù)中的兩個。整個通話的安全,只取決于第三個隨機數(shù)(Premaster secret)
能不能被破解。為了足夠安全,我們可以考慮把握手階段的算法從默認的RSA
算法,改為 Diffie-Hellman
算法(簡稱DH算法)。采用DH算法后,Premaster secret
不需要傳遞,雙方只要交換各自的參數(shù),就可以算出這個隨機數(shù)。
4、protobuffer
protobuffer是一種語言無關、平臺無關的數(shù)據(jù)協(xié)議,優(yōu)點在于壓縮性好,可擴展,標準化,常用于數(shù)據(jù)傳輸、持久化存儲等。
4.1 實現(xiàn)原理
4.1.1 protobuffer協(xié)議
1.壓縮性好(相比于同樣跨平臺、跨語言的json)
- 去除字段定義,分隔符(引號,冒號,逗號)
- 壓縮數(shù)字,因為日常經(jīng)常使用到的比較小的數(shù)字,實際有效的字節(jié)數(shù)沒有4個字節(jié)
- 采用TLV的數(shù)據(jù)存儲方式:減少了分隔符的使用 & 數(shù)據(jù)存儲得緊湊
varint和zigzag算法:對數(shù)字進行壓縮
protobuffer協(xié)議去除字段定義,分隔符
2.可拓展
protobuffer并不是一個自解析的協(xié)議(json自解析key),需要pb的meta數(shù)據(jù)解析,犧牲了可讀性,但在大規(guī)模的數(shù)據(jù)處理是可以接受的。可拓展性在于protobuffer中追加定義,新舊版本是可以兼容的,但是定義是嚴格有序的。
3.標準化
protobuffer生態(tài)提供了完整的工具鏈,protobuffer提供官方的生成golang/java等實現(xiàn)插件,這樣protobuffer字節(jié)碼的各語言版本的序列化、反序列化是標準化的,也包括插件生成代碼封裝。以及標準化的常用工具,比如doc-gen、grpc-gen、grpc-gateway-gen等工具。
4.1.2 典型應用
gRPC
protobuffer封裝
需要注意:
1.pb結(jié)構封裝的字段標序是不能更改的,否則解析錯亂;
2.pb結(jié)構盡量把必選類型,比如int、bool放在filedNum<=16;可得到更好的varint壓縮效果;
3.grpc-client使用時,req是指針類型,務必不要重復復制,盡量new request,否則編碼時會錯亂;
4.2 protobuffer 編碼原理
protobuffer(以下簡稱為PB)兩個重要的優(yōu)勢在于高效的序列化/反序列化和低空間占用,而這兩大優(yōu)勢是以其高效的編碼方式為基礎的。PB底層以二進制形式存儲,比binary struct方式更加緊湊,一般來講空間利用率也比binary struct更高,是以Key-Value的形式存儲【圖1】。
PB示例結(jié)構如下:
//示例protobuf結(jié)構 message Layer1 { optional uint32 layer1_varint = 1; optional Layer2 layer1_message = 2; } message Layer2 { optional uint32 layer2_varint = 1; optional Layer3 layer2_message = 2; } message Layer3 { optional uint32 layer3_varint = 1; optional bytes layer3_bytes = 2; optional float layer3_float = 3; optional sint32 layer3_sint32 = 4; }
PB將常用類型按存儲特性分為數(shù)種Wire Type【圖 2】, Wire Type不同,編碼方式不同。根據(jù)Wire Type(占3bits)和field number生成Key。
Key計算方式如下,并以Varint編碼方式序列化【參考下面Varint編碼】,所以理論上[1, 15]范圍內(nèi)的field, Key編碼后占用一個字節(jié), [16,)的Key編碼后會占用一個字節(jié)以上,所以盡量避免在一個結(jié)構里面定義過多的field。如果碰到需要定義這么多field的情況,可以采用嵌套方式定義。
//Key的計算方式 Key = field_number << 3 | Wire_Type
Type | Value | Meaning | Contain |
---|---|---|---|
Varint | 0 | varint | int32,int64,sint32,sint64,uint32,uint64,bool,enum |
Fixed64 | 1 | 64-bit | fixed64,sfixed64,double,float |
LengthDelimited | 2 | length-delimi | string,message,bytes,repeated |
StartGroup | 3 | start group | groups(deprecated) |
EndGroup | 4 | end group | groups(deprecated) |
Fixed32 | 5 | 32-bit | fixed32,float |
4.2.1 Varint編碼
inline uint8* WriteVarint32ToArray(uint32 value, uint8* target) { while (value >= 0x80) { *target = static_cast<uint8>(value | 0x80); value >>= 7; ++target; } *target = static_cast<uint8>(value); return target + 1; }
Layer1 obj; obj.set_layer1_varint(12345);
Varint編碼只用每個字節(jié)的低7bit存儲數(shù)據(jù),最高位0x80用做標識,清空:最后一個字節(jié),置位:還有數(shù)據(jù)。以上述操作為例,設uint32類型為12345,其序列化過程如下:
該字段的field_number = 1
, wire_type = 0
, 則key = 1 << 3 | 0 = 0x20
。那么在內(nèi)存中, 其序列為應該為0x20B960,占3Bytes。對比直接用struct存儲會占4Bytes;
如果struct是用 uint64呢,將占8Bytes,而PB占用內(nèi)存仍是3Bytes。
下表是PB數(shù)值范圍與其字節(jié)數(shù)對應關系。實際應用中,我們用到的數(shù)大概率是比較小的,而且可能 動態(tài)范圍比較大(有時需要用64位存儲),對比struct的內(nèi)存占用,PB優(yōu)勢很明顯。
數(shù)值范圍 | 占用字節(jié)數(shù) |
---|---|
0-127 | 2 |
128-16383 | 3 |
16384-2097151 | 4 |
2097152-268435455 | 5 |
4.2.2 ZigZag編碼
ZigZag編碼是對Varint編碼的補充與優(yōu)化。負數(shù)在內(nèi)存中以前補碼形式存在,但不管是負數(shù)的原碼還是補碼,最高位都是1;那么問題來了,如果以上述Varint編碼方式,所有負數(shù)序列化以后都會以最大化占用內(nèi)存(32位占用6Bytes, 64位占用11Btyes)。所以,細心的同學會發(fā)現(xiàn),對于有符號數(shù)的表示有兩種類型,int32和sint32。對,sint32就是對這種負數(shù)序列化優(yōu)化的變種。
inline uint32 WireFormatLite::ZigZagEncode32(int32 n) { // Note: the right-shift must be arithmetic return (static_cast<uint32>(n) << 1) ^ (n >> 31); }
sint32 | uint32 |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
… | … |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
對于sint32類型的數(shù)據(jù),在varint編碼之前,會先進行zigzag編碼,上圖是其映射關系。編碼后,較小的負數(shù),可以映射為較小的正數(shù),從而實現(xiàn)根據(jù)其信息量決定其序列化后占用的內(nèi)存大小。
所以聰明的同學們已經(jīng)知道該如何選擇了,對于有符合數(shù)盡量選擇sint32,而不是int32,不管從空間和時間上,都是更優(yōu)的選擇
4.2.3 length-delimi編碼
length-delimi編碼方式比較簡單,是建立在varint編碼基礎上的,主要是針對message、bytes、repeated等類型,與TLV格式類似。先以varint編碼寫入tag即Key,再以varint編碼寫入長度,最后把內(nèi)容memcpy到內(nèi)存中。
inline uint8* WriteBytesToArray(int field_number, const string& value, uint8* target) { target = WriteTagToArray(field_number, WIRETYPE_LENGTH_DELIMITED, target); return io::CodedOutputStream::WriteStringWithSizeToArray(value, target); } uint8* WriteStringWithSizeToArray(const string& str, uint8* target) { GOOGLE_DCHECK_LE(str.size(), kuint32max); target = WriteVarint32ToArray(str.size(), target); return WriteStringToArray(str, target); }
4.2.4 fixed編碼
fixed編碼很簡單,主要針對類型有fixed32、fixed64、double、float。由于長度固定,只需要Key + Value即可。對于浮點型會先強制轉(zhuǎn)換成相對應的整形,反序列化時則反之。
inline uint32 EncodeFloat(float value) { union {float f; uint32 i;}; f = value; return i; } inline uint64 EncodeDouble(double value) { union {double f; uint64 i;}; f = value; return i; } inline void WireFormatLite::WriteFloatNoTag(float value, io::CodedOutputStream* output) { output->WriteLittleEndian32(EncodeFloat(value)); } inline void WireFormatLite::WriteDoubleNoTag(double value, io::CodedOutputStream* output) { output->WriteLittleEndian64(EncodeDouble(value)); }
4.2.5 整個編碼流程
4.3 protobuf解碼過程
上面已經(jīng)介紹了編碼原理,那么解碼的流程也就很簡單了。解碼是一個遞歸的過程,先通過Varint解碼過程讀出Key, 取出field_number字段,如果不存在于message中,就放到UnKnownField中。如果是認識的field_number,則根據(jù)wire_type做具體的解析。對于普通類型(如整形,bytes, fixed類型等)就直接寫入Field中,如果是嵌套類型(一般特指嵌套的Message),則遞歸調(diào)用整個解析過程。解析完一個繼續(xù)解析下一個,直到buffer結(jié)束。
5、gzip壓縮方案
5.1 gzip介紹
gzip是GNU zip的縮寫,它是一個GNU自由軟件的文件壓縮程序,也經(jīng)常用來表示gzip這種文件格式。軟件的作者是Jean-loup Gailly和Mark Adler。1992年10月31日第一次公開發(fā)布,版本號是0.1,目前的穩(wěn)定版本是1.2.4。
Gzip主要用于Unix系統(tǒng)的文件壓縮。我們在Linux中經(jīng)常會用到后綴為.gz的文件,它們就是GZIP格式的。現(xiàn)今已經(jīng)成為Internet 上使用非常普遍的一種數(shù)據(jù)壓縮格式,或者說一種文件格式。 當應用Gzip壓縮到一個純文本文件時,效果是非常明顯的,經(jīng)過GZIP壓縮后頁面大小可以變?yōu)樵瓉淼?0%甚至更小,這取決于文件中的內(nèi)容。
HTTP協(xié)議上的GZIP編碼是一種用來改進WEB應用程序性能的技術。web開發(fā)中可以通過gzip壓縮頁面來降低網(wǎng)站的流量,而gzip并不會對cpu造成大量的占用,略微上升,也是幾個百分點而已,但是對于頁面卻能壓縮30%以上,非常劃算。
利用Apache中的Gzip模塊,我們可以使用Gzip壓縮算法來對Apache服務器發(fā)布的網(wǎng)頁內(nèi)容進行壓縮后再傳輸?shù)娇蛻舳藶g覽器。這樣經(jīng)過壓縮后實際上降低了網(wǎng)絡傳輸?shù)淖止?jié)數(shù)(節(jié)約傳輸?shù)木W(wǎng)絡I/o),最明顯的好處就是可以加快網(wǎng)頁加載的速度。
網(wǎng)頁加載速度加快的好處不言而喻,除了節(jié)省流量,改善用戶的瀏覽體驗外,另一個潛在的好處是Gzip與搜索引擎的抓取工具有著更好的關系。例如 Google就可以通過直接讀取gzip文件來比普通手工抓取更快地檢索網(wǎng)頁。在Google網(wǎng)站管理員工具(Google Webmaster Tools)中你可以看到,sitemap.xml.gz 是直接作為Sitemap被提交的。
而這些好處并不僅僅限于靜態(tài)內(nèi)容,PHP動態(tài)頁面和其他動態(tài)生成的內(nèi)容均可以通過使用Apache壓縮模塊壓縮,加上其他的性能調(diào)整機制和相應的服務器端緩存規(guī)則,這可以大大提高網(wǎng)站的性能。因此,對于部署在Linux服務器上的PHP程序,在服務器支持的情況下,我們建議你開啟使用Gzip Web壓縮。
5.2 Web服務器處理HTTP壓縮的過程如下:
- Web服務器接收到瀏覽器的HTTP請求后,檢查瀏覽器是否支持HTTP壓縮(Accept-Encoding 信息);
- 如果瀏覽器支持HTTP壓縮,Web服務器檢查請求文件的后綴名;
- 如果請求文件是HTML、CSS等靜態(tài)文件,Web服務器到壓縮緩沖目錄中檢查是否已經(jīng)存在請求文件的最新壓縮文件;
- 如果請求文件的壓縮文件不存在,Web服務器向瀏覽器返回未壓縮的請求文件,并在壓縮緩沖目錄中存放請求文件的壓縮文件;
- 如果請求文件的最新壓縮文件已經(jīng)存在,則直接返回請求文件的壓縮文件;
- 如果請求文件是動態(tài)文件,Web服務器動態(tài)壓縮內(nèi)容并返回瀏覽器,壓縮內(nèi)容不存放到壓縮緩存目錄中。
下面是兩個演示圖:
未使用Gzip:
開啟使用Gzip后:
5.3 啟用apache的gzip功能
Apache上利用Gzip壓縮算法進行壓縮的模塊有兩種:mod_gzip 和mod_deflate。要使用Gzip Web壓縮,請首先確定你的服務器開啟了對這兩個組件之一的支持。
雖然使用Gzip同時也需要客戶端瀏覽器的支持,不過不用擔心,目前大部分瀏覽器都已經(jīng)支持Gzip了,如IE、Mozilla Firefox、Opera、Chrome等。
通過查看HTTP頭,我們可以快速判斷使用的客戶端瀏覽器是否支持接受gzip壓縮。若發(fā)送的HTTP頭中出現(xiàn)以下信息,則表明你的瀏覽器支持接受相應的gzip壓縮:
Accept-Encoding: gzip 支持mod_gzip Accept-Encoding: deflate 支持mod_deflate Accept-Encoding: gzip,deflate 同時支持mod_gzip 和mod_deflate
如firebug查看:
Accept-Encoding: gzip,deflate 是同時支持mod_gzip 和mod_deflate
如果服務器開啟了對Gzip組件的支持,那么我們就可以在http.conf或.htaccess里面進行定制,下面是一個.htaccess配置的簡單實例:
mod_gzip 的配置:
# mod_gzip: <ifModule mod_gzip.c> mod_gzip_on Yes mod_gzip_dechunk Yes mod_gzip_item_include file .(html?|txt|css|js|php|pl)$ mod_gzip_item_include handler ^cgi-script$ mod_gzip_item_include mime ^text/.* mod_gzip_item_include mime ^application/x-javascript.* mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.* <ifModule>
mod_deflate的配置實例:
打開打開apache 配置文件httpd.conf
將#LoadModule deflate_module modules/mod_deflate.so去除開頭的#號
# mod_deflate: <ifmodule mod_deflate.c> DeflateCompressionLevel 6 #壓縮率, 6是建議值. AddOutputFilterByType DEFLATE text/plain AddOutputFilterByType DEFLATE text/html AddOutputFilterByType DEFLATE text/xml AddOutputFilterByType DEFLATE text/css AddOutputFilterByType DEFLATE text/javascript AddOutputFilterByType DEFLATE application/xhtml+xml AddOutputFilterByType DEFLATE application/xml AddOutputFilterByType DEFLATE application/rss+xml AddOutputFilterByType DEFLATE application/atom_xml AddOutputFilterByType DEFLATE application/x-javascript AddOutputFilterByType DEFLATE application/x-httpd-php AddOutputFilterByType DEFLATE image/svg+xml </ifmodule>
里面的文件MIME類型可以根據(jù)自己情況添加,至于PDF 、圖片、音樂文檔之類的這些本身都已經(jīng)高度壓縮格式,重復壓縮的作用不大,反而可能會因為增加CPU的處理時間及瀏覽器的渲染問題而降低性能。所以就沒必要再通過Gzip壓縮。通過以上設置后再查看返回的HTTP頭,出現(xiàn)以下信息則表明返回的數(shù)據(jù)已經(jīng)過壓縮。即網(wǎng)站程序所配置的Gzip壓縮已生效。
Content-Encoding: gzip
firebug查看:
注意:
1)不管使用mod_gzip 還是mod_deflate,此處返回的信息都一樣。因為它們都是實現(xiàn)的gzip壓縮方式。
2)CompressionLevel 9是指壓縮程度的等級(設置壓縮比率),取值范圍在從1到9,9是最高等級。據(jù)了解,這樣做最高可以減少8成大小的傳輸量(看檔案內(nèi)容而定),最少也能夠節(jié)省一半。 CompressionLevel 預設可以采用 6 這個數(shù)值,以維持耗用處理器效能與網(wǎng)頁壓縮質(zhì)量的平衡. 不建議設置太高,如果設置很高,雖然有很高的壓縮率,但是占用更多的CPU資源. 3) 對已經(jīng)是壓縮過的圖片格式如jpg,音樂檔案如mp3、壓縮文件如zip之類的,就沒必要再壓縮了。
5.4 mod_gzip 和mod_deflate的主要區(qū)別是什么?使用哪個更好呢?
第一個區(qū)別是安裝它們的Apache Web服務器版本的差異:
Apache 1.x系列沒有內(nèi)建網(wǎng)頁壓縮技術,所以才去用額外的第三方mod_gzip 模塊來執(zhí)行壓縮。
Apache 2.x官方在開發(fā)的時候,就把網(wǎng)頁壓縮考慮進去,內(nèi)建了mod_deflate 這個模塊,用以取代mod_gzip。雖然兩者都是使用的Gzip壓縮算法,它們的運作原理是類似的。
第二個區(qū)別是壓縮質(zhì)量:
mod_deflate 壓縮速度略快而mod_gzip 的壓縮比略高。一般默認情況下,mod_gzip 會比mod_deflate 多出4%~6%的壓縮量。
那么,為什么使用mod_deflate?
第三個區(qū)別是對服務器資源的占用:
一般來說mod_gzip 對服務器CPU的占用要高一些。mod_deflate 是專門為確保服務器的性能而使用的一個壓縮模塊,mod_deflate 需要較少的資源來壓縮文件。這意味著在高流量的服務器,使用mod_deflate 可能會比mod_gzip 加載速度更快。
不太明白?簡而言之,如果你的網(wǎng)站,每天不到1000獨立訪客,想要加快網(wǎng)頁的加載速度,就使用mod_gzip。雖然會額外耗費一些服務器資源, 但也是值得的。如果你的網(wǎng)站每天超過1000獨立訪客,并且使用的是共享的虛擬主機,所分配系統(tǒng)資源有限的話,使用mod_deflate 將會是更好的選擇。
另外,從Apache 2.0.45開始,mod_deflate 可使用DeflateCompressionLevel 指令來設置壓縮級別。該指令的值可為1(壓縮速度最快,最低的壓縮質(zhì)量)至9(最慢的壓縮速度,壓縮率最高)之間的整數(shù),其默認值為6(壓縮速度和壓縮質(zhì) 量較為平衡的值)。這個簡單的變化更是使得mod_deflate 可以輕松媲美m(xù)od_gzip 的壓縮。
P.S. 對于沒有啟用以上兩種Gzip模塊的虛擬空間,還可以退而求其次使用php的zlib函數(shù)庫(同樣需要查看服務器是否支持)來壓縮文件,只是這種方法使用起來比較麻煩,而且一般會比較耗費服務器資源,請根據(jù)情況慎重使用。
5.5 zlib.output_compression和ob_gzhandler編碼方式壓縮
服務器不支持mod_gzip、mod_deflate模塊,若想通過GZIP壓縮網(wǎng)頁內(nèi)容,可以考慮兩種方式,開啟zlib.output_compression或者通過ob_gzhandler編碼的方式。
1)zlib.output_compression是在對網(wǎng)頁內(nèi)容壓縮的同時發(fā)送數(shù)據(jù)至客戶端。
2)ob_gzhandler是等待網(wǎng)頁內(nèi)容壓縮完畢后才進行發(fā)送,相比之下前者效率更高,但需要注意的是,兩者不能同時使用,只能選其一,否則將出現(xiàn)錯誤。
兩者的實現(xiàn)方式做簡單描述:
5.5.1 zlib.output_compression實現(xiàn)方式
在默認情況下,zlib.output_compression是關閉:
; Transparent output compression using the zlib library ; Valid values for this option are 'off', 'on', or a specific buffer size ; to be used for compression (default is 4KB) ; Note: Resulting chunk size may vary due to nature of compression. PHP ; outputs chunks that are few hundreds bytes each as a result of ; compression. If you prefer a larger chunk size for better ; performance, enable output_buffering in addition. ; Note: You need to use zlib.output_handler instead of the standard ; output_handler, or otherwise the output will be corrupted. ; http://php.net/zlib.output-compression zlib.output_compression = Off ; http://php.net/zlib.output-compression-level ;zlib.output_compression_level = -1
如需開啟需編輯php.ini文件,加入以下內(nèi)容:
zlib.output_compression = On zlib.output_compression_level = 6
可以通過phpinfo()函數(shù)檢測結(jié)果。
當zlib.output_compression的Local Value和MasterValue的值同為On時,表示已經(jīng)生效,這時候訪問的PHP頁面(包括偽靜態(tài)頁面)已經(jīng)GZIP壓縮了,通過Firebug或者在線網(wǎng)頁GZIP壓縮檢測工具可檢測到壓縮的效果。
5.5.2 ob_gzhandler的實現(xiàn)方式
如果需要使用ob_gzhandler,則需關閉zlib.output_compression,把php.ini文件內(nèi)容更改為:
zlib.output_compression = Off zlib.output_compression_level = -1
通過在PHP文件中插入相關代碼實現(xiàn)GZIP壓縮P壓縮:
if (extension_loaded('zlib')) { if ( !headers_sent() AND isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) //頁面沒有輸出且瀏覽器可以接受GZIP的頁面 { ob_start('ob_gzhandler'); } } //待壓縮的內(nèi)容 echo $context; ob_end_flush();
如何瀏覽器提示:內(nèi)容編碼錯誤,應該是:
使用ob_start('ob_gzhandler')時候前面已經(jīng)有內(nèi)容輸出,檢查前面內(nèi)容以及require include調(diào)用文件的內(nèi)容。若無法找到可以在調(diào)用其它文件前使用ob_start(),調(diào)用之后使用 ob_end_clean () 來清除輸出的內(nèi)容:
if (extension_loaded('zlib')) { if ( !headers_sent() AND isset($_SERVER['HTTP_ACCEPT_ENCODING']) && strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) //頁面沒有輸出且瀏覽器可以接受GZIP的頁面 { ob_end_clean (); ob_start('ob_gzhandler'); } }
或者我們使用gzencode來壓縮:
<?php $encoding = 'gzip'; $content = '123456789'; ob_end_clean (); header('Content-Encoding: '.$encoding); $result = gzencode($content); echo $result; exit;
不管是zlib.output_compression還是ob_gzhandler,都僅能對PHP文件進行GZIP壓縮,對于HTML、CSS、JS等靜態(tài)文件只能通過調(diào)用PHP的方式實現(xiàn)。
最后想說的是,現(xiàn)在主流的瀏覽器默認使用的是HTTP1.1協(xié)議,基本都支持GZIP壓縮,對于IE而言,假如你沒有選中其菜單欄工具-》Internet 選項-》高級-》HTTP 1.1 設置-》使用 HTTP 1.1,那么,你將感受不到網(wǎng)頁壓縮后的速度提升所帶來的快感!
以上就是Android性能優(yōu)化之弱網(wǎng)優(yōu)化詳解的詳細內(nèi)容,更多關于Android 性能弱網(wǎng)優(yōu)化的資料請關注腳本之家其它相關文章!
相關文章
AndroidManifest.xml <uses-feature>和<uses-permisstio
這篇文章主要介紹了AndroidManifest.xml <uses-feature>和<uses-permisstion>分析及比較的相關資料,需要的朋友可以參考下2017-06-06詳解Android開發(fā)之MP4文件轉(zhuǎn)GIF文件
這篇文章介紹的是將錄下來的視頻選取一小段轉(zhuǎn)為 GIF 文件,不僅時間段可以手動選取,而且還需要支持截取視頻的局部區(qū)域轉(zhuǎn)為 GIF,網(wǎng)上調(diào)研了一下技術方案,覺得還是有必要把實現(xiàn)過程拿出來分享下,有需要的可以直接拿過去用。下面來一起看看。2016-08-08Android 游戲引擎libgdx 資源加載進度百分比顯示案例分析
因為案例比較簡單,所以簡單用AndroidApplication -> Game -> Stage 搭建框架感興趣的朋友可以參考下2013-01-01Android實現(xiàn)點擊獲取驗證碼60秒后重新獲取功能
這篇文章主要為大家詳細介紹了Android點擊獲取驗證碼60秒后重新獲取驗證碼的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06Android開發(fā)筆記SQLite優(yōu)化記住密碼功能
這篇文章主要為大家詳細介紹了Android開發(fā)筆記SQLite優(yōu)化記住密碼功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07Android實現(xiàn)將已發(fā)送的短信寫入短信數(shù)據(jù)庫的方法
這篇文章主要介紹了Android實現(xiàn)將已發(fā)送的短信寫入短信數(shù)據(jù)庫的方法,是Android手機開發(fā)常見的技巧,需要的朋友可以參考下2014-09-09