欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Android性能優(yōu)化之弱網(wǎng)優(yōu)化詳解

 更新時間:2022年10月19日 10:14:17   作者:塞爾維亞大漢  
這篇文章主要為大家介紹了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ù)組而重寫writeObjectreadObject

    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
TypeValueMeaningContain
Varint0varintint32,int64,sint32,sint64,uint32,uint64,bool,enum
Fixed64164-bitfixed64,sfixed64,double,float
LengthDelimited2length-delimistring,message,bytes,repeated
StartGroup3start groupgroups(deprecated)
EndGroup4end groupgroups(deprecated)
Fixed32532-bitfixed32,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-1272
128-163833
16384-20971514
2097152-2684354555

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);
}
sint32uint32
00
-11
12
-23
21474836474294967294
-21474836484294967295

對于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)化的資料請關注腳本之家其它相關文章!

相關文章

最新評論