安卓程式碼、圖片、佈局、網路和電量優化

豌豆射手_BiuBiu發表於2018-08-16
  • 本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

寫在前面的話,前段時間寫了一篇文章 二十三種設計模式,寫的不詳細,因為如果要寫的很詳細,估計一年半載都寫不完,完全都是按照自己理解,每個設計模式就畫了一個簡單的圖,同時完成了一個小Demo,哪知道這篇文章成了我在簡書點贊最高的一篇文章,實在有點受寵若驚,謝謝各位大佬點贊!!!

這篇文章分為五個部分程式碼優化、圖片優化、佈局優化、網路優化、電量優化,儘量每個方法都寫了小的Demo!

image.png

Demo下載的地址

F0xJ.png

程式碼優化:不要做多餘的工作,儘量避免次數過多的記憶體的分配,(需要對api有一定的熟悉)

資料集合的使用:建議最佳的做法是可能使用ArrayList作為首選,只要你需要使用額外的功能的時候,或者當程式效能由於經常從表的中間進行插入和刪除而變差的時候,才會去選擇LinkedList。HashMap效能上於HashTable相當,因為HashMap和HashTable在底層的儲存和查詢機制是一樣的,但是TreeMap通常比HashMap要慢。HashSet總體上的效能比TreeSet好,特別實在新增和查詢元素的時候,而這兩個操作也是最重要的操作。TreeSet存在的唯一的原因是它可以維持元素的排序的狀態,所以當需要一個排好序的Set,才使用TreeSet。因為其內部的結果歐支援排序,並且因為迭代是我們更有可能執行的操作,所以,用TreeSet迭代通常比用HashSet要快。

  /**
         * 建議最佳的做法是可能使用ArrayList作為首選,只要你需要使用額外的功能的時候,或者當程式效能由於經常從表的中間進行
         * 插入和刪除而變差的時候,才會去選擇LinkedList
         */
        //資料結構的選擇,對於ArrayList,插入的操作特別高昂,並且其代價將隨著列表的尺寸的增加而增加
        ArrayList list=new ArrayList();
        //需要執行大量的隨機的訪問,這個不是一個好的選擇,如果是使用迭代器在列表中插入新的資料,使用這個,比較低廉(插入和移除的代價比較低廉)
        LinkedList linkedList=new LinkedList();
        //HashMap效能上於HashTable相當,因為HashMap和HashTable在底層的儲存和查詢機制是一樣的,但是TreeMap通常比HashMap要慢
        HashMap<String,String> hashMap=new HashMap<>();
        /**
         * HashSet總體上的效能比TreeSet好,特別實在新增和查詢元素的時候,而這兩個操作也是最重要的操作。TreeSet存在的唯一的原因是它
         * 可以維持元素的排序的狀態,所以當需要一個排好序的Set,才使用TreeSet。因為其內部的結果歐支援排序,並且因為迭代是我們更有可能
         * 執行的操作,所以,用TreeSet迭代通常比用HashSet要快
         */
        HashSet<String>  hashSet=new HashSet<>();

        TreeSet<String> treeSet=new TreeSet<>();
複製程式碼

SparseArray是Android特有的稀疏陣列的實現,他是Integer和Object的為例進行的一個對映用於代替 HsahMap<Integer,>,提高效能。

  • SparseArray
  • 執行緒不安全(多執行緒中需要注意)
  • 由於要進行二分查詢,(可以是有序的),SparseArray會對插入的資料按照Key的大小順序插入
  • SparseArray對刪除操作做了優化,它並不會立刻刪除這個元素,而是通過設定標記位(DELETED)的方法,後面嘗試重用。

內部核心的實現(二分查詢)

 /**
     * 二分查詢也稱折半查詢(Binary Search),它是一種效率較高的查詢方法。
     * 但是,折半查詢要求線性表必須採用順序儲存結構,而且表中元素按關鍵字有序排列。
     * @param array
     * @param size
     * @param value
     * @return
     */
    //二分查詢
    static int binarySearch(int[] array, int size, int value) {
        int lo = 0;
        int hi = size - 1;

        while (lo <= hi) {
            /**
             * >>>與>>唯一的不同是它無論原來的最左邊是什麼數,統統都用0填充。
             * —比如你的例子,byte是8位的,-1表示為byte型是11111111(補碼錶示法)
             * b>>>4就是無符號右移4位,即00001111,這樣結果就是15。
             * 這裡相當移動一位,除以二
             */
            final int mid = (lo + hi) >>> 1;
            final int midVal = array[mid];

            if (midVal < value) {
                lo = mid + 1;
            } else if (midVal > value) {
                hi = mid - 1;
            } else {
                return mid;  // value found
            }
        }
        //按位取反(~)運算子 ,沒有找到,這個資料
        return ~lo;  // value not present
    }
複製程式碼

SpareArray 家族有以下的四類

          //SpareArray 家族有以下的四類
        //用於替換    HashMap<Integer,boolean>
        SparseBooleanArray sparseBooleanArray=new SparseBooleanArray();
        sparseBooleanArray.append(1,false);
        //用於替換    HashMap<Integer,Interger>
        SparseIntArray SparseIntArray=new SparseIntArray();
        SparseIntArray.append(1,1);
        //用於替換    HashMap<Integer,boolean>

        @SuppressLint({"NewApi", "LocalSuppress"})
        SparseLongArray SparseLongArray=new SparseLongArray();
        SparseLongArray.append(1,1111000L);
        //用於替換    HashMap<Integer,boolean>
        SparseArray<String> SparseArray11=new SparseArray<String>();
        SparseArray11.append(1,"dd");
複製程式碼

SpareArray中的設計模式:原型模式:這裡有使用到了的,原型模式記憶體中複製資料的,不會呼叫到類的構造的方法,而且訪問的許可權對原型模式無效

  • 優點: 1、效能提高。 2、逃避建構函式的約束。
  • 缺點: 1、配備克隆方法需要對類的功能進行通盤考慮,這對於全新的類不是很難,但對於已有的類不一定很容易,特別當一個類引用不支援序列化的間接物件,或者引用含有迴圈結構的時候。 2、必須實現 Cloneable 介面。
    SparseArray<String> clone = sparseArray.clone();
複製程式碼

Handler正確的使用姿勢(☺☺☺) 下面的程式碼是很多人都會這樣寫,這樣會造成記憶體洩漏 原因:Handler是和Looper以及MessageQueue一起工作的,在安卓中,一個 應用啟動了,系統會預設建立一個主執行緒服務的Looper物件 ,該Looper物件處理主執行緒的所有的Message訊息,他的生命週期貫穿整個應用。在主執行緒中使用的Handler的都會預設的繫結到這個looper的物件,咋主執行緒中建立handler的時候,它會立即關聯主執行緒Looper物件的MessageQueue,這時傳送到的MessageQueue 中的Message物件都會持有這個Handler的物件的引用,這樣Looper處理訊息時Handler的handlerMessage的方法,因此,如果Message還沒有處理完成,那麼handler的物件不會立即被垃圾回收

   /*-------------old ide 已經告訴我們這裡可能記憶體洩露-------------------*/
    @SuppressLint("HandlerLeak")
    private final Handler mHandler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

//        mHandler.postDelayed(new Runnable() {
//            @Override
//            public void run() {
//                // TODO: 2018/4/28  使用者即使退出了應用的話,這裡也是會執行的 ,通過日記的觀察
//                //這裡有可能使用者退出了Activity
//                System.out.println("shiming mHandler --todo");
//            }
//        },5000);
複製程式碼

如何避免,有兩點的可以嘗試

  • 1、在子執行緒中使用Handler,但是Handler不能再子執行緒中使用,需要開發者自己建立一個Looper物件,實現難,方法怪
  • 2、將handler宣告為靜態的內部類,靜態內部類不會持有外部類的引用,因此,也不會引起記憶體洩露,

    InnerHandler innerHandler = new InnerHandler(this);
        innerHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                //這裡這要 退出了 就不會執行了
                System.out.println("shiming innerHandler --todo");

            }
        },5000);
  public class InnerHandler extends Handler{
        //弱應用,在另外一個地方會講到
        private final WeakReference<HandlerActivity> mActivityWeakReference;

        public InnerHandler(HandlerActivity activity){
            mActivityWeakReference=new WeakReference<HandlerActivity>(activity);
        }
    }
複製程式碼

Context正確的姿勢

   //Context的種類
        //Application 全域性唯一的Context例項
        Application application = getApplication();
        Context applicationContext = application.getApplicationContext();
        //不同的Activity,得到這個Context,是獨立的,不會進行復用
        Context baseContext = this.getBaseContext();

        MyBroadcaseRecriver myBroadcaseRecriver = new MyBroadcaseRecriver();

        //ContentProvider 中的Context

        /**
         *如果建立單利必須需要使用到context物件
         */
        //這樣不會記憶體洩露,不用改動單利類中程式碼
        SingleInstance.getSingleInstance(getApplication().getApplicationContext());
複製程式碼
  • 單例模式,如果不得不傳入Context,由於單例一直存在會導致Activity或者是Service的單例引用,從而不會被垃圾回收, Activity中的關聯的View和資料結構也不會被釋放,正確的方式應該使用Application中的Context
class SingleInstance {
    private static SingleInstance sSingleInstance;
    private final Context mContext;

    private SingleInstance(Context context){
        mContext = context;
    }
//    因為每次呼叫例項都需要判斷同步鎖,很多專案包括很多人都是用這種的
//    雙重判斷校驗的方法,這種的方法看似很完美的解決了效率的問題,但是它
//    在併發量不多,安全性不太高的情況下能完美的執行,但是,
//    在jvm編譯的過程中會出現指令重排的優化過程,這就會導致singleton實際上
//    沒有被初始化,就分配了記憶體空間,也就是說singleton!=null但是又沒有被初始化,
//    這就會導致返回的singletonthird返回的是不完整的
    public static SingleInstance getSingleInstance(Context context){
        if (sSingleInstance==null){
            synchronized (SingleInstance.class){
                if (sSingleInstance==null)   {
                    // TODO: 2018/4/28 注意外面傳入的conext物件是否,是哪個 
                    sSingleInstance= new SingleInstance(context);
                    //第二種是改動程式碼,使用application 中的context變數
                    sSingleInstance= new SingleInstance(context.getApplicationContext());
                    
                }
            }

        }
        return sSingleInstance;
    }
}
複製程式碼
  • java 四種引用方式和引用佇列的解釋 和java一樣,Android也是基於垃圾回收(GC)機制實現記憶體的自動的回收,垃圾回收的演算法“標記-清除(Mark-Sweep)” “標記壓縮(Mark-Compact)“複製演算法(Copying)以及引用計數演算法(Reference-Counting),安卓的虛擬機器(Dalvik還是Art),都是使用標記清除演算法。 在Android中,記憶體洩露是指不再使用的物件依然佔有記憶體,或者是他們佔用的記憶體沒有得到釋放, 從而導致記憶體空間不斷的減少,由於可用的空間比較少,發生記憶體洩露會使得記憶體更加的緊張,甚至最終由於記憶體耗盡而發生的OOM,導致應用的崩潰。
         * 和java一樣,Android也是基於垃圾回收(GC)機制實現記憶體的自動的回收,垃圾回收的演算法“標記-清除(Mark-Sweep)”
         * “標記壓縮(Mark-Compact)“複製演算法(Copying)以及引用計數演算法(Reference-Counting),安卓的虛擬機器(Dalvik還是Art),
         * 都是使用標記清除演算法”
         *

        mTextView1.setText(des1);

        /**
         * 在Android中,記憶體洩露是指不再使用的物件依然佔有記憶體,或者是他們佔用的記憶體沒有得到釋放,
         * 從而導致記憶體空間不斷的減少,由於可用的空間比較少,發生記憶體洩露會使得記憶體更加的緊張,
         * 甚至最終由於記憶體耗盡而發生的OOM,導致應用的崩潰
         */

        mTextView2.setText(des2);
複製程式碼
  • 強引用:Java中裡面最廣泛的使用的一種,也是物件預設的引用型別,如果又一個物件具有強引用,那麼垃圾回收器是不會對它進行回收操作的,當記憶體的空間不足的時候,Java虛擬機器將會拋OutOfMemoryError錯誤,這時應用將會被終止執行
  • 軟引用:一個物件如果只有一個軟引用,那麼當記憶體空間充足是,垃圾回收器不會對他進行回收操作,只有當記憶體空間不足的時候,這個物件才會被回收,軟引用可以用來實現記憶體敏感的快取記憶體,如果配合引用佇列(ReferenceQueue使用,當軟引用指向物件被垃圾回收器回收後,java會把這個軟引用加入到與之關聯的引用佇列中)
  • 弱引用:弱引用是比軟引用更弱的一種的引用的型別,只有弱引用指向的物件的生命週期更短,當垃圾回收器掃描到只有具有弱引用的物件的時候,不敢當前空間是否不足,都會對弱引用物件進行回收,當然弱引用也可以和一個佇列配合著使用
  • 引用佇列:ReferenceQueue一般是作為WeakReference SoftReference 的構造的函式引數傳入的,在WeakReference 或者是 softReference 的指向的物件被垃圾回收後,ReferenceQueue就是用來儲存這個已經被回收的Reference

        String des3="強引用:Java中裡面最廣泛的使用的一種,也是物件預設的引用型別,如果又一個物件具有強引用,那麼垃圾回收器是不會對它進行回收操作的,當記憶體的空間不足的時候,Java虛擬機器將會丟擲OutOfMemoryError錯誤,這時應用將會被終止執行";

        mTextView3.setText(des3);
        String des4="軟引用:一個物件如果只有一個軟引用,那麼當記憶體空間充足是,垃圾回收器不會對他進行回收操作,只有當記憶體空間不足的時候,這個物件才會被回收,軟引用可以用來實現記憶體敏感的快取記憶體,如果配合引用佇列(ReferenceQueue使用,當軟引用指向物件被垃圾回收器回收後,java會把這個軟引用加入到與之關聯的引用佇列中)";

        Object obj=new Object();

        SoftReference<Object> sr = new SoftReference<>(obj);//這裡使用了軟引用...
        /*
         *在這個期間,有可能會出現記憶體不足的情況發生,那麼GC就會直接把所有的軟引用全部清除..並釋放記憶體空間
         *如果記憶體空間足夠的話,那麼就GC就不會進行工作...
         *GC的工作取決於記憶體的大小,以及其內部的演算法,,,,
         */
        if(sr!=null){
            //如果軟引用還存在,那麼直接就可以獲取這個物件的相關資料...這樣就實現了cache...
            obj = sr.get();

        }else{
            //如果已經不存在,表示GC已經將其回收,我們需要重新例項化物件,獲取資料資訊...
            obj = new Object();
            sr = new SoftReference<>(obj);
        }
        mTextView4.setText(des4);
        String des5="弱引用:弱引用是比軟引用更弱的一種的引用的型別,只有弱引用指向的物件的生命週期更短,當垃圾回收器掃描到只有具有弱引用的物件的時候,不敢當前空間是否不足,都會對弱引用物件進行回收,當然弱引用也可以和一個佇列配合著使用";

        Object obj1 = new Object();
        WeakReference<Object> weakProductA = new WeakReference<>(obj1);


        mTextView5.setText(des5);
        String des6="虛引用:和軟引用和弱引用不同,虛引用並不會對所指向的物件生命週期產生任何影響,也就是物件還是會按照它原來的方式別垃圾回收期回收,虛引用本質上只是有一個標記作用,主要用來跟蹤物件被垃圾回收的活動,虛引用必須和引用佇列配合使用,當物件被垃圾回收時,如果存在虛引用,那麼Java虛擬機器會將這個虛引用加入到與之關聯的引用佇列中";

        mTextView6.setText(des6);
        /**
         * 如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
         * 虛引用主要用來跟蹤物件被垃圾回收器回收的活動
         */
        // TODO: 2018/5/2 程式可以通過判斷引用佇列中是否已經加入了虛引用,
        // 來了解被引用的物件是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,
        // 那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。
        ReferenceQueue queue = new ReferenceQueue ();
        PhantomReference pr = new PhantomReference<Object>(obj1, queue);


        String des7="引用佇列:ReferenceQueue一般是作為WeakReference SoftReference 的構造的函式引數傳入的,在WeakReference 或者是 softReference 的指向的物件被垃圾回收後,ReferenceQueue就是用來儲存這個已經被回收的Reference";
        mTextView7.setText(des7);

複製程式碼
  • 將HashMap封裝成一個執行緒安全的集合,並且使用軟引用的方式防止OOM(記憶體不足)。由於在ListView中會載入大量的圖片.那麼為了有效的防止OOM導致程式終止的情況
  /**
     * list中使用大量的bitmap,這種情況的話,我自己感覺使用的比較少
     */
    public class MemoryCache {
        //將HashMap封裝成一個執行緒安全的集合,並且使用軟引用的方式防止OOM(記憶體不足)...
        //由於在ListView中會載入大量的圖片.那麼為了有效的防止OOM導致程式終止的情況...
        private Map<String,SoftReference<Bitmap>> cache=Collections.synchronizedMap(new HashMap<String, SoftReference<Bitmap>>());

        public Bitmap get(String id){
            if(!cache.containsKey(id))
                return null;
            SoftReference<Bitmap>ref=cache.get(id);
            return ref.get();
        }

        public void put(String id,Bitmap bitmap){
            cache.put(id, new SoftReference<Bitmap>(bitmap));
        }
        public void clear(){
            cache.clear();
        }
    }
複製程式碼

一個簡單的Demo

/**
 * author: Created by shiming on 2018/5/2 14:50
 * mailbox:lamshiming@sina.com
 */
public class EmployeeCache {
    static private EmployeeCache cache;// 一個Cache例項
    private Hashtable<String, EmployeeRef> employeeRefs;// 用於Chche內容的儲存
    private ReferenceQueue<Employee> q;// 垃圾Reference的佇列

     // 繼承SoftReference,使得每一個例項都具有可識別的標識。
    // 並且該標識與其在HashMap內的key相同。
    public class EmployeeRef extends SoftReference<Employee> {
        private String _key = "";
        public EmployeeRef(Employee em, ReferenceQueue<Employee> q) {
            super(em, q);
            _key = em.getID();
        }
    }

    // 構建一個快取器例項
    private EmployeeCache() {
        employeeRefs = new Hashtable<String, EmployeeRef>();
        q = new ReferenceQueue<Employee>();
    }

    // 取得快取器例項
    public static EmployeeCache getInstance() {
        if (cache == null) {
            cache = new EmployeeCache();
        }
        return cache;
    }

    // 以軟引用的方式對一個Employee物件的例項進行引用並儲存該引用
    private void cacheEmployee(Employee em) {
        cleanCache();// 清除垃圾引用
        EmployeeRef ref = new EmployeeRef(em, q);
        employeeRefs.put(em.getID(), ref);
    }

    // 依據所指定的ID號,重新獲取相應Employee物件的例項
    public Employee getEmployee(String ID) {
        Employee em = null;
       // 快取中是否有該Employee例項的軟引用,如果有,從軟引用中取得。
        if (employeeRefs.containsKey(ID)) {
            EmployeeRef ref = (EmployeeRef) employeeRefs.get(ID);
            em = (Employee) ref.get();
        }
       // 如果沒有軟引用,或者從軟引用中得到的例項是null,重新構建一個例項,
       // 並儲存對這個新建例項的軟引用
        if (em == null) {
            em = new Employee(ID);
            System.out.println("Retrieve From EmployeeInfoCenter. ID=" + ID);
            this.cacheEmployee(em);
        }
        return em;
    }

    // 清除那些所軟引用的Employee物件已經被回收的EmployeeRef物件
    private void cleanCache() {
        EmployeeRef ref = null;
        while ((ref = (EmployeeRef) q.poll()) != null) {
            employeeRefs.remove(ref._key);
        }
    }

    // 清除Cache內的全部內容
    public void clearCache() {
        cleanCache();
        employeeRefs.clear();
        //告訴垃圾收集器打算進行垃圾收集,而垃圾收集器進不進行收集是不確定的
        System.gc();
        //強制呼叫已經失去引用的物件的finalize方法
        System.runFinalization();
    }

    /**
     * 當垃圾收集器認為沒有指向物件例項的引用時,會在銷燬該物件之前呼叫finalize()方法。
     * 該方法最常見的作用是確保釋放例項佔用的全部資源。java並不保證定時為物件例項呼叫該方法,
     * 甚至不保證方法會被呼叫,所以該方法不應該用於正常記憶體處理。
     * @throws Throwable
     */
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
    }
}


/**
 * author: Created by shiming on 2018/5/2 14:49
 * mailbox:lamshiming@sina.com
 */

public class Employee {
    private String id;// 僱員的標識號碼
    private String name;// 僱員姓名
    private String department;// 該僱員所在部門
    private String Phone;// 該僱員聯絡電話
    private int salary;// 該僱員薪資
    private String origin;// 該僱員資訊的來源

    // 構造方法
    public Employee(String id) {
        this.id = id;
        getDataFromlnfoCenter();
    }
    // 到資料庫中取得僱員資訊
    private void getDataFromlnfoCenter() {
// 和資料庫建立連線井查詢該僱員的資訊,將查詢結果賦值
// 給name,department,plone,salary等變數
// 同時將origin賦值為"From DataBase"
    }

    public String getID() {
        return id;
    }
}
複製程式碼

其他需要注意到的地方:

  • 1、不要重複的建立相同的物件,物件的建立都是需要記憶體分配的,物件的銷燬需要垃圾回收,這些都在一定程度上影響程式的效能
  • 2、對常量使用static final修飾,對於基本型別和String型別的常量,建議使用常量static final 修飾,因為final型別的常量會在靜態dex檔案的域初始化部分,這時對基本資料型別和String型別常量的呼叫不會涉及類的初始化,而是直接呼叫字面量
  • 3、避免內部的get set方法的呼叫,get set的作用是對以外遮蔽具體的變數定義,從而達到更好的封裝性,如果在類的內部呼叫get set的方法訪問變數的話,會降低訪問的速度,根據在安卓的官方的文件,在沒有jit編譯器時,直接訪問變數的速度是呼叫get方法的3倍,在jit編譯器,直接訪問變數是呼叫get方法的7倍,當然使用了ProGuard的話,perGuard會對get set 進行內聯的操作,從而達到直接訪問的效果

關於JIT:

  • JIT是”Just In Time Compiler”的縮寫,就是”即時編譯技術”,與Dalvik虛擬機器相關,JIT是在2.2版本提出的,目的是為了提高Android的執行速度,一直存活到4.4版本,因為在4.4之後的ROM中,就不存在Dalvik虛擬機器了。
  • 編譯打包APK檔案:1、Java編譯器將應用中所有Java檔案編譯為class檔案,2、dx工具將應用編譯輸出的類檔案轉換為Dalvik位元組碼,即dex檔案
  • Google在2.2版本新增了JIT編譯器,當App執行時,每當遇到一個新類,JIT編譯器就會對這個類進行編譯,經過編譯後的程式碼,會被優化成相當精簡的原生型指令碼(即native code),這樣在下次執行到相同邏輯的時候,速度就會更快。
  • dex位元組碼翻譯成本地機器碼是發生在應用程式的執行過程中的,並且應用程式每一次重新執行的時候,都要做重做這個翻譯工作,所以這個工作並不是一勞永逸,每次重新開啟App,都需要JIT編譯,Dalvik虛擬機器從Android一出生一直活到4.4版本,而JIT在Android剛釋出的時候並不存在,在2.2之後才被新增到Dalvik中。
  • AOT是”Ahead Of Time”的縮寫,指的就是ART(Anroid RunTime)這種執行方式。
  • JIT是執行時編譯,這樣可以對執行次數頻繁的dex程式碼進行編譯和優化,減少以後使用時的翻譯時間,雖然可以加快Dalvik執行速度,但是還是有弊病,那就是將dex翻譯為本地機器碼也要佔用時間,所以Google在4.4之後推出了ART,用來替換Dalvik。
  • ART的策略與Dalvik不同,在ART 環境中,應用在第一次安裝的時候,位元組碼就會預先編譯成機器碼,使其成為真正的本地應用。之後開啟App的時候,不需要額外的翻譯工作,直接使用本地機器碼執行,因此執行速度提高。
  • 當然ART與Dalvik相比,還是有缺點的。
    • ART需要應用程式在安裝時,就把程式程式碼轉換成機器語言,所以這會消耗掉更多的儲存空間,但消耗掉空間的增幅通常不會超過應用程式碼包大小的20%
    • 由於有了一個轉碼的過程,所以應用安裝時間難免會延長
    • 但是這些與更流暢的Android體驗相比而言,不值一提。

圖片優化

四種圖片格式

  • JPEG

    • 是一種廣泛使用的有失真壓縮影象標準格式,它不支援透明和多幀動畫,一般攝影的作品是JEPG格式的,通過控制壓縮比,可以調整圖片的大小
  • PNG

    • 是一種無失真壓縮的圖片格式,他支援完整的透明通道,從圖片處理的領域來講,JEPG只有RGB三個通道,而PNG有ARGB四個通道,因此PNG圖片佔用空間一般比較大,會無形的增加app的大小,在做app瘦身時一般都要對PNG圖片進行梳理以減小其佔用的體積
  • GIF * 是一種古老的圖片的格式,誕生於1987年,隨著初代網際網路流行開來,他的特別是支援多幀動畫,表情圖,

  • Webp

    • google於2010年釋出,支援有損和無損、支援完整的透明通道、也支援多幀動畫,目前主流的APP都已經使用了Webp,淘寶,微信,即保證了圖片的大小和質量
  • 在安卓應用開發中能夠使用編解碼格式的只有三種 JEPG PNG WEBP

   /**
     * 在安卓應用開發中能夠使用編解碼格式的只有三種 JEPG PNG WEBP
     */
    public enum CompressFormat {
        JPEG    (0),
        PNG     (1),
        WEBP    (2);//安卓4.0後開始支援

        CompressFormat(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;
    }
複製程式碼

推薦幾種圖片處理網站

  • 無失真壓縮ImageOptin,在不犧牲圖片質量的前提下,即減下來PNG圖片佔用的空間,又提高了圖片的載入速度 imageoptim.com/api

    智圖.png

  • 有失真壓縮ImageAlpha,圖片大小得到極大的縮小,如果需要使用的話,一定要ui設計師看能否使用 pngmini.com/

  • 有失真壓縮TinyPNG 比較知名的png壓縮的工具,也需要ui設計師看能夠使用不 tinypng.com/

  • PNG/JPEG 轉化為 wepb :智圖 :zhitu.isux.us/

如果ui設計師工作量不飽和的話,可以推薦, 儘量使用 .9.png 點9圖 小黑點表示 可拉伸區域,黑邊表示縱向顯示內容的範圍

####佈局優化:如果建立的層級結構比較複雜,View樹巢狀的層次比較深,那麼將會使得頁面的響應的時間變長,導致執行的時候越來越慢

  • merge標籤(對安卓的事件傳遞要達到原始碼級的熟悉才可以理解) 在某些場景下可以減少佈局的層次,由於所有的Activity的根佈局都是FrameLayout Window PhoneWindow DecorView 事件的傳遞,包括設定setContentView 等的方法---> 我會寫一篇文章獨立解釋安卓事件的原始碼解析,會更加清楚的介紹這個類,(對安卓的事件傳遞要達到原始碼級的熟悉才可以理解)todo<-----所以,當獨立的一個佈局檔案最外層是FrameLayout的時候,並且和這個佈局不需要設定 background 或者 padding的時候,可以使用標籤來代替FrameLayout佈局。另外一種的情況可以使用《merge》便籤的情況是當前佈局作為另外一個佈局的子佈局
  <include android:layout_height="50dp"
      android:layout_width="match_parent"
      layout="@layout/layout_include_merge"
      />


<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView android:gravity="center"
        android:text="merge 標籤 在某些場景下可以減少佈局的層次,由於所有的"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</merge>
複製程式碼
  • 在安卓中經常會使用到相同的佈局,比如說title,最佳的實踐的方法就是把相同的佈局抽取出來,獨立成一個xml檔案,需要使用到的時候,就把這個佈局include進來,不僅減少了程式碼量,而且修改這個相同的佈局,只需要修改一個地方即可.

  • ViewStub 是一種不可見的並且大小為0的試圖,它可以延遲到執行時才填充inflate 佈局資源,當Viewstub設為可見或者是inflate的時候,就會填充佈局資源,這個佈局和普通的試圖就基本上沒有任何區別,比如說,載入網路失敗,或者是一個比較消耗效能的功能,需要使用者去點選才可以載入,參考我的開源的專案 WritingPen

注意事項:如果這個根佈局是個View,比如說是個ImagView,那麼找出來的id為null,得必須注意這一點 ###---------->2018.6.7修正這個說法,以前我說的是錯誤的,根本上的原因是ViewStub設定了 inflateid ,這才是更本身的原因,對不起!搞錯了,還是要看原始碼

   <ViewStub
        android:padding="10dp"
        android:background="@color/colorPrimary"
        android:layout_gravity="center"
        android:inflatedId="@+id/find_view_stub"
        android:id="@+id/view_stub"
        android:layout="@layout/view_stub_imageview"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />



<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:padding="10dp"
    android:src="@drawable/ic_launcher_background"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:text="如果這個根佈局是個View,比如說是個ImagView,那麼找出來的id為null,得必須注意這一點"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <!--如果這個根佈局是個View,比如說是個ImagView,那麼找出來的id為null,得必須注意這一點-->
    <ImageView
        android:layout_marginTop="20dp"
        android:id="@+id/imageview"
        android:padding="10dp"
        android:src="@drawable/ic_launcher_background"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</FrameLayout>
複製程式碼

呼叫todo: 2018/5/4 為啥為null 原因是佈局檔案中根佈局只有View,沒有ViewGroup,ViewStub.inflate() 的方法和 setVisibility 方法是差不多,因為 setVisibility方法會(看原始碼)走這個inflate的方法

if (null!=mViewStub.getParent()){
                   /*
                   android:inflatedId 的值是Java程式碼中呼叫ViewStub的 inflate()或者是serVisibility方法返回的Id,這個id就是被填充的View的Id
                    */
                   /**
                    * ViewStub.inflate() 的方法和 setVisibility 方法是差不多,因為 setVisibility方法會(看原始碼)走這個inflate的方法
                    */
//                    View inflate = mViewStub.inflate();
                   mViewStub.setVisibility(View.VISIBLE);
                   //inflate--->android.support.v7.widget.AppCompatImageView{de7e3a2 V.ED..... ......I. 0,0-0,0 #7f07003e app:id/find_view_stub}
//                    System.out.println("shiming inflate--->"+inflate);
                   final View find_view_stub = findViewById(R.id.find_view_stub);
                   System.out.println("shiming ----"+find_view_stub);


                   View iamgeivew11 = find_view_stub.findViewById(R.id.imageview);
                   //himing ---- iamgeivew11null
                   // TODO: 2018/5/4 為啥為null  原因是佈局檔案中根佈局只有View,沒有ViewGroup
                   System.out.println("shiming ---- iamgeivew11"+iamgeivew11);

               }else{
                   Toast.makeText(LayoutOptimizationActivity.this,"已經inflate了",Toast.LENGTH_LONG).show();
                   final View viewById = findViewById(R.id.find_view_stub);
                   View iamgeivew = findViewById(R.id.imageview);
                   //已經inflate了android.support.v7.widget.AppCompatImageView{4637833 V.ED..... ........ 348,294-732,678 #7f07003e app:id/find_view_stub}
                   System.out.println("shiming l----已經inflate了"+viewById);//
                   System.out.println("shiming l----已經inflate了iamgeivew"+iamgeivew);//已經inflate了iamgeivew==null
                   View iamgeivew11 = viewById.findViewById(R.id.imageview);
                   //已經inflate了 iamgeivew11null
                   System.out.println("shiming l----已經inflate了 iamgeivew11"+iamgeivew11);
               }
           }
複製程式碼
  • 儘量使用CompoundDrawable,如果存在相鄰的ImageView和TextView 的話
<LinearLayout
        android:layout_width="match_parent"
        android:layout_height="150dp">
    <TextView
        android:text="我是文字"
        android:drawableBottom="@mipmap/ic_launcher_round"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
   <TextView
       android:text="我是title2"
       android:drawableEnd="@mipmap/ic_launcher_round"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:drawableRight="@mipmap/ic_launcher_round" />
    <TextView
        android:text="我是文字33"
        android:drawableLeft="@mipmap/ic_launcher_round"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:drawableStart="@mipmap/ic_launcher_round" />

    <TextView
        android:drawableTop="@mipmap/ic_launcher_round"
        android:text="我是文字3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    </LinearLayout>

複製程式碼

image.png

網路優化. 移動端對額App幾乎都是聯網的,網路延遲等會對App的效能產生較大的影響,網路優化可以節約網路流量和電量

  • 2018年7月2日增加上網過程的說明流程(最近面試別人,發現好多同學這個都說的不太明白,特此說明下):對於普通的上網,系統是這樣做的:瀏覽器本身就是一個客戶端,當你輸入URL的時候,首先瀏覽器會去請求DNS伺服器,通過DNS獲取相應域名的對應的Ip地址,通過IP地址找到對應Ip對應的伺服器,要求建立TCP連線,等瀏覽器傳送完HTTP Request包後,伺服器接受到請求包之後才開始處理請求包,伺服器呼叫自身服務,返回Http Response (響應包):客戶端收到來自伺服器的響應後開始渲染這個Response包裡的主體(body),等收到全部的內容隨後斷開與該伺服器之間的TCP連線。具體的文章在這裡Web工作的方式

圖解

  • DNS域名的系統,主要的功能根據應用請求所用的域名URL去網路上面對映表中查相對應的IP地址,這個過程有可能會消耗上百毫秒,而且可能存在著DNS劫持的危險,可以替換為Ip直接連線的方式來代替域名訪問的方法,從而達到更快的網路請求,但是使用Ip地址不夠靈活,當後臺變換了Ip地址的話,會出現訪問不了,前段的App需要發包,解決方法是增加Ip地址動態更新的能力,或者是在IP地址訪問失敗了,切換到域名的訪問.

Demo--->ping 一個地址,不正確的話,切換到備用的地址

  boolean ping = ping("wwww.baidu.com");

 /**
     * 測試主域名是否可用
     *
     * @param ip
     * @return
     */
    private final int PING_TIME_OUT = 1000; // ping 超時時間
    private boolean ping(String ip) {
        try {
            Integer status = executeCommandIp( ip, PING_TIME_OUT );
            if ( status != null && status == 0 ) {
                return true;
            } else {
                return false;
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        return false;
    }
   /**
     * 執行域名是否可通
     * @param command
     * @param timeout
     * @return
     * @throws IOException
     * @throws InterruptedException
     * @throws TimeoutException
     */
    private int executeCommandIp( final String command, final long timeout )
            throws IOException, InterruptedException, TimeoutException {
        Process process = Runtime.getRuntime().exec(
                "ping -c 1 -w 100 " + command);
        mWorker = new PingWorker(process);
        mWorker.start();
        try {
            mWorker.join(timeout);
            if (mWorker.exit != null) {
                return mWorker.exit;
            } else {
                //throw new TimeoutException();
                return -1;
            }
        } catch (InterruptedException ex) {
            mWorker.interrupt();
            Thread.currentThread().interrupt();
            throw ex;
        } finally {
            process.destroy();
        }
    }
複製程式碼

PingWorker 類

 class PingWorker extends Thread {
        private final Process process;
        private Integer exit;
        private String ip;

        public PingWorker(Process process) {
            this.process = process;
        }

        @Override
        public void run() {
            try {
                exit = process.waitFor();
                if (exit == 0) {
                    BufferedReader buf = new BufferedReader(new InputStreamReader(process.getInputStream()));
                    String str = new String();
                    StringBuffer ipInfo = new StringBuffer();

                    //讀出所有資訊並顯示
                    while((str=buf.readLine())!=null) {
                        ipInfo.append(str);
                    }
                    /*
                    PING sni1st.dtwscache.ourwebcdn.com (14.215.228.4) 56(84) bytes of data.64 bytes from 14.215.228.4: icmp_seq=1 ttl=57 time=16.6 ms--- sni1st.dtwscache.ourwebcdn.com ping statistics ---1 packets transmitted, 1 received, 0% packet loss, time 0msrtt min/avg/max/mdev = 16.656/16.656/16.656/0.000 ms
                     */
                    System.out.println("shiming ipInfo----->"+ipInfo);
                    Pattern mPattern = Pattern.compile("\\((.*?)\\)");
                    Matcher matcher = mPattern.matcher(ipInfo.toString());
                    if ( matcher.find() ) {
                        ip = matcher.group( 1 );
                    }
                }
                else {
                    ip = " process.waitFor()==="+exit;
                }
            }
            catch (IOException e) {
                e.printStackTrace();
                ip="java.io.IOException: Stream closed";
                return;
            }
            catch (InterruptedException e) {
                ip="java.io.InterruptedException: Stream closed";
                return;
            }
        }
    }
複製程式碼
  • 合併網路請求,一次完整的Http請求,首先進行的是DNS查詢,通過TCP三次握手,從而建立連線,如果是https請求的話,還要經過TLS握手成功後才可以進行連線,對於網路請求,減少介面,能夠合併的網路請求就儘量合併
    • SSL(Secure Sockets Layer 安全套接層),及其繼任者傳輸層安全(Transport Layer Security,TLS)是為網路通訊提供安全及資料完整性的一種安全協議。TLS與SSL在傳輸層對網路連線進行加密。

HTTPS和HTTP的區別主要為以下四點:

  • 一、https協議需要到ca申請證照,一般免費證照很少,需要交費。

  • 二、http是超文字傳輸協議,資訊是明文傳輸,https 則是具有安全性的ssl加密傳輸協議。

  • 三、http和https使用的是完全不同的連線方式,用的埠也不一樣,前者是80,後者是443。

  • 四、http的連線很簡單,是無狀態的;HTTPS協議是由SSL+HTTP協議構建的可進行加密傳輸、身份認證的網路協議,比http協議安全。

  • 預先獲取資料能夠將網路請求集中在一次,這樣其他時間段手機就可以切換到空閒的時間,從而避免經常性的喚醒,從而節約用電

  • 避免輪詢:如果說每個一段時間需要向伺服器發起主動的網路請求,其實不建議在app端做這樣的操作,可以使用推送,如果說在不得已的情況下,也要避免使用Thread.sleep()函式來迴圈等待,建議使用系統的AlarmManager來實現定時輪詢,AlarmManager 可以保證在系統休眠的時候,CPU也可以得到休息,在下一次需要發起網路球球的時候才喚醒

  • 儘量避免網路請求失敗時候,無限制的迴圈重試連線,在我第一篇簡書部落格有寫過一個網路載入的框架 :www.jianshu.com/p/141ee58eb… 中有提到過


  //基於Rxjava 和 RxAndroid Retorfit
          o.subscribeOn(Schedulers.io())
                .retryWhen(new RetryWhenHandler(1, 5))
                .doOnSubscribe(new Action0() {
                    @Override
                    public void call() {
                        s.onBegin();
                    }
                })
                .subscribeOn(AndroidSchedulers.mainThread())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(s);
複製程式碼
  • 離線快取,對於圖片或者檔案,記憶體快取+磁碟快取+網路快取,一般我們本地需要做的是二級快取,當快取中存在圖片或者是檔案,直接從快取中讀取,不會走網路,下載圖片,在Android中使用LruCache實現記憶體快取,DiskLruCache實現本地快取
   /**
     * 圖片快取的核心類
     */
    private LruCache<String, Bitmap> mLruCache;
    // 快取大小
    private static final int CACHE_MAX_SIZE = 1024;

    /**
     * LRU(Least recently used,最近最少使用)演算法根據資料的歷史訪問記錄來進行淘汰資料,其核心思想是“如果資料最近被訪問過,那麼將來被訪問的機率也更高”。
     */
    private void lruCacheDemo() {
        // 獲取應用程式最大可用記憶體
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        //設定LruCache快取的大小,一般為當前程式可用容量的1/8。
        int cacheSize = maxMemory / 8;
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            //重寫sizeOf方法,計算出要快取的每張圖片的大小
            //這個方法要特別注意,跟我們例項化 LruCache 的 maxSize 要呼應,怎麼做到呼應呢,比如 maxSize 的大小為快取的個數,這裡就是 return 1就 ok,如果是記憶體的大小,如果5M,這個就不能是個數 了,這是應該是每個快取 value 的 size 大小,如果是 Bitmap,這應該是 bitmap.getByteCount();
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight();
            }
            ////這裡使用者可以重寫它,實現資料和記憶體回收操作
            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                if (oldValue != newValue) {
                    oldValue.recycle();
                }
            }
        };
    }
    /**
     * 從LruCache中獲取一張圖片,如果不存在就返回null。
     */
    private Bitmap getBitmapFromLruCache(String key) {
        return mLruCache.get(key);
    }
    /**
     * 往LruCache中新增一張圖片
     *
     * @param key
     * @param bitmap
     */
    private void addBitmapToLruCache(String key, Bitmap bitmap) {
        if (getBitmapFromLruCache(key) == null) {
            if (bitmap != null)
                mLruCache.put(key, bitmap);
        }
    }
複製程式碼
  • 壓縮資料的大小:可以對傳送服務端資料進行gzip壓縮,同時可以使用更優的資料傳輸格式,例如二進位制的代替Json格式,這個比較牛逼,估計運用的很少,使用webp格式代替圖片格式

  • 不同的網路環境使用不同的超時策略,常見的網路格式有 2g、3g、4g、wifi,實時的更新當前的網路狀態,通過監聽來獲取最新的網路型別,並動態調整網路超時的時間

 private void netWorkDemo() {
        TextView netWork = findViewById(R.id.net_work);
        boolean networkConnected = NetworkUtils.isNetworkConnected(this);
        int networkType = NetworkUtils.getNetworkType(this);

        System.out.println("shiming 是否聯網了"+networkConnected);
        switch (networkType){
            case TYPE_UNKNOWN:
                System.out.println("shiming 聯網的型別---無網路連線");
                netWork.setText("是否聯網了---》"+networkConnected+" 聯網的型別---無網路連線");
                break;
            case TYPE_2G:
                System.out.println("shiming 聯網的型別---2G");
                netWork.setText("是否聯網了---》"+networkConnected+" 聯網的型別---2G");
                break;
            case TYPE_3G:
                System.out.println("shiming 聯網的型別---TYPE_3G");
                netWork.setText("是否聯網了---》"+networkConnected+" 聯網的型別---TYPE_3G");
                break;
            case TYPE_4G:
                System.out.println("shiming 聯網的型別---TYPE_4G");
                netWork.setText("是否聯網了---》"+networkConnected+" 聯網的型別---TYPE_4G");
                break;
            case TYPE_WIFI:
                System.out.println("shiming 聯網的型別---TYPE_WIFI");
                netWork.setText("是否聯網了---》"+networkConnected+" 聯網的型別---TYPE_WIFI");
                break;
        }
    }
複製程式碼

NetworkUtils 類

package com.shiming.performanceoptimization.network_optimization;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.telephony.TelephonyManager;



/**
 * author: Created by shiming on 2018/4/28 10:52
 * mailbox:lamshiming@sina.com
 * des:網路連線工具類
 */

public class NetworkUtils {
    private static final String SUBTYPE_TD_SCDMA = "SCDMA";
    private static final String SUBTYPE_WCDMA = "WCDMA";
    private static final String SUBTYPE_CDMA2000 = "CDMA2000";

    /**
     * 判斷是否已連線到網路.
     *
     * @param context Context
     * @return 是否已連線到網路
     */
    public static boolean isNetworkConnected(Context context) {
        ConnectivityManager connectivity = (ConnectivityManager) context.getSystemService(Context
                .CONNECTIVITY_SERVICE);
        if (connectivity != null) {
            NetworkInfo info = connectivity.getActiveNetworkInfo();
            if (info != null && info.isConnected()) {
                if (info.getState() == NetworkInfo.State.CONNECTED) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 獲取當前網路型別
     *
     * @param context Context
     * @return 當前網路型別(Unknown, 2G, 3G, 4G, WIFI)
     */
    public static int getNetworkType(Context context) {
        NetworkInfo info = ((ConnectivityManager) context.getSystemService(
                Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo();
        if (info != null && info.isConnected()) {
            if (info.getType() == ConnectivityManager.TYPE_WIFI) {
                return NetworkType.TYPE_WIFI;
            } else if (info.getType() == ConnectivityManager.TYPE_MOBILE) {
                switch (info.getSubtype()) {
                    case TelephonyManager.NETWORK_TYPE_GPRS:
                    case TelephonyManager.NETWORK_TYPE_EDGE:
                    case TelephonyManager.NETWORK_TYPE_CDMA:
                    case TelephonyManager.NETWORK_TYPE_1xRTT:
                    case TelephonyManager.NETWORK_TYPE_IDEN: //api<8 : replace by 11
                        return NetworkType.TYPE_2G;

                    case TelephonyManager.NETWORK_TYPE_UMTS:
                    case TelephonyManager.NETWORK_TYPE_EVDO_0:
                    case TelephonyManager.NETWORK_TYPE_EVDO_A:
                    case TelephonyManager.NETWORK_TYPE_HSDPA:
                    case TelephonyManager.NETWORK_TYPE_HSUPA:
                    case TelephonyManager.NETWORK_TYPE_HSPA:
                    case TelephonyManager.NETWORK_TYPE_EVDO_B: //api<9 : replace by 14
                    case TelephonyManager.NETWORK_TYPE_EHRPD:  //api<11 : replace by 12
                    case TelephonyManager.NETWORK_TYPE_HSPAP:  //api<13 : replace by 15
                        return NetworkType.TYPE_3G;

                    case TelephonyManager.NETWORK_TYPE_LTE:    //api<11 : replace by 13
                        return NetworkType.TYPE_4G;

                    default:
                        // http://baike.baidu.com/item/TD-SCDMA 中國移動 聯通 電信 三種3G制式
                        String subtypeName = info.getSubtypeName();
                        if (SUBTYPE_TD_SCDMA.equalsIgnoreCase(subtypeName) ||
                                SUBTYPE_WCDMA.equalsIgnoreCase(subtypeName) ||
                                SUBTYPE_CDMA2000.equalsIgnoreCase(subtypeName)) {
                            return NetworkType.TYPE_3G;
                        } else {
                            return NetworkType.TYPE_UNKNOWN;
                        }
                }
            }
        }
        return NetworkType.TYPE_UNKNOWN;
    }
}

複製程式碼

NetworkType類

package com.shiming.performanceoptimization.network_optimization;

/**
 * author: Created by shiming on 2018/4/28 10:52
 * mailbox:lamshiming@sina.com
 * des:網路連線型別常量
 */

public class NetworkType {
    /**
     * 無網路連線
     */
    public static final int TYPE_UNKNOWN = -1;
    /**
     * 2G
     */
    public static final int TYPE_2G = 0;
    /**
     * 3G
     */
    public static final int TYPE_3G = 1;
    /**
     * 4G
     */
    public static final int TYPE_4G = 2;
    /**
     * WIFI
     */
    public static final int TYPE_WIFI = 3;
}
複製程式碼
  • CDN的全稱是Content Delivery Network,即內容分發網路。其基本思路是儘可能避開網際網路上有可能影響資料傳輸速度和穩定性的瓶頸和環節,使內容傳輸的更快、更穩定。通過在網路各處放置節點伺服器所構成的在現有的網際網路基礎之上的一層智慧虛擬網路,CDN系統能夠實時地根據網路流量和各節點的連線、負載狀況以及到使用者的距離和響應時間等綜合資訊將使用者的請求重新導向離使用者最近的服務節點上。其目的是使使用者可就近取得所需內容,解決 Internet網路擁擠的狀況,提高使用者訪問網站的響應速度。

####電量優化

  • 1、(BroadCastReceiver)為了減少應用耗損的電量,我們在程式碼中儘量避免使用無用的操作程式碼,當應用退到後臺了,一切頁面的重新整理都是沒有意義的,並且浪費電,比如有個監聽網路狀態的廣播並執行一些動作,彈窗或者是Toast,那麼app需要在後臺的時候,禁用掉這個功能,

  • 2、資料傳輸 藍芽傳輸,Wi-Fi傳輸 行動網路傳輸 後臺資料的管理:根據業務需求,介面儘量避免無效資料的傳輸 資料傳輸的頻度問題:通過經驗值或者是資料統計的方法確定好資料傳輸的頻度,避免冗餘重複的資料傳輸,資料傳輸過程中要壓縮資料的大小,合併網路請求,避免輪詢

  • 3、位置服務 正確的使用位置復位,是應用耗電的一個關鍵

  • 需要注意以下的三點:

    • 1、有沒有及時登出位置監聽器:和廣播差不多

    • 2、位置更新監聽頻率的設定;更加業務需求設定一個合理的更新頻率值,

      • minTime:用來指定間更新通知的最小的時間間隔,單位是毫秒,看日誌這裡是1s更新的
      • minDistance:用來指定位置更新通知的最小的距離,單位是米
    • 3、Android提供了三種定位

      • GPS定位,通過GPS實現定位,精度最高,通常在10米(火星座標),但是GPS定位在時間和電量上消耗也是最高的
      • 網路定位,通過行動通訊的基站訊號差異來計算出手機所在的位置,精度比GPS差好多
      • 被動定位,最省電的定位服務,如果使用被動定位服務。說明它想知道位置更新資訊但有不主動獲取,等待手機中其他應用或者是服務或者是系統元件發出定位請求,並和這些元件的監聽器一起接收位置的資訊,實際的開發中,一般使用的是第三方的地圖,高德,騰訊,百度,他們做了很好的封裝,同時在地圖上的表現上更加的優化
        /**
         *   //設定定位精確度 Criteria.ACCURACY_COARSE比較粗略,Criteria.ACCURACY_FINE則比較精細
         *         criteria.setAccuracy(Criteria.ACCURACY_FINE);
         *         //設定是否要求速度
         *         criteria.setSpeedRequired(false);
         *         // 設定是否允許運營商收費
         *         criteria.setCostAllowed(false);
         *         //設定是否需要方位資訊
         *         criteria.setBearingRequired(false);
         *         //設定是否需要海拔資訊
         *         criteria.setAltitudeRequired(false);
         *         // 設定對電源的需求
         *         criteria.setPowerRequirement(Criteria.POWER_LOW);
         */
        Criteria criteria = new Criteria();
        criteria.setAccuracy(Criteria.ACCURACY_FINE);
        criteria.setAltitudeRequired(false);
        criteria.setBearingRequired(false);
        criteria.setCostAllowed(true);
        criteria.setPowerRequirement(Criteria.POWER_LOW);
        String serviceName = Context.LOCATION_SERVICE;
        mLocationManager = (LocationManager) getSystemService(serviceName);
//        locationManager.setTestProviderEnabled("gps", true);
        // TODO: 2018/5/3 IllegalArgumentException 'Provider "gps" unknown"   https://www.cnblogs.com/ok-lanyan/archive/2011/10/12/2208378.html
        mLocationManager.addTestProvider(LocationManager.GPS_PROVIDER,
                "requiresNetwork" == "", "requiresSatellite" == "", "requiresCell" == "", "hasMonetaryCost" == "",
                "supportsAltitude" == "", "supportsSpeed" == "",
                "supportsBearing" == "", android.location.Criteria.POWER_LOW,
                android.location.Criteria.ACCURACY_FINE);
        mProvider = mLocationManager.getBestProvider(criteria, true);

        //獲取緯度
        //獲取經度
        mLlistener = new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                // thread is not runable, msg ignore, state:TIMED_WAITING, 這裡的執行緒有可能ANR
                if (location != null) {
                    double lat = location.getLatitude();//獲取緯度
                    double lng = location.getLongitude();//獲取經度
                    System.out.println("shiming   lat+" + lat);
                    System.out.println("shiming   lng+" + lng);
                    String name = Thread.currentThread().getName();
                    mCount++;
                    System.out.println("當前執行緒的位置name---"+name+"i==="+mCount);
                    mTv_location.setText("位置資訊是2s變動的,可以設定,我是第"+mCount+"次變動的--->"+"\n\r"+"lat===="+lat+"      lng----->"+lng);
                }
                if (mLocationManager!=null) {
                    mLocationManager.removeUpdates(this);
                    if (mLocationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
                        mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 2000, 1000, mLlistener);
                    }else {
                        mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 1000, mLlistener);
                    }
//                     TODO: 2018/5/3  這裡在報錯了,我把他註釋掉
//                mLocationManager.setTestProviderEnabled(mProvider, false);//   java.lang.IllegalArgumentException: Provider "network" unknown
                }
            }

            @Override
            public void onProviderDisabled(String provider) {

            }

            @Override
            public void onProviderEnabled(String provider) {

            }

            @Override
            public void onStatusChanged(String provider, int status,
                                        Bundle extras) {

            }
        };
        /**
         * minTime:用來指定間更新通知的最小的時間間隔,單位是毫秒,看日誌這裡是1s更新的
         * minDistance:用來指定位置更新通知的最小的距離,單位是米
         */
        mLocationManager.requestLocationUpdates(mProvider, 2000, (float) 1000.0, mLlistener);

複製程式碼

在OnDestroy 變數手動值為null,我在測試過程中,只有在值為null的時候這個位置監聽才會停止,有興趣的小夥伴,可以好好看看值為null,底層會做什麼操作

/**
     * 記得需要銷燬這個監聽,
     * todo 如果不手動置為null的話,其實您可以通過日記發現,這個監聽還是一直在走的,所以說這裡手動值為null的好處
     */
    protected void onDestroy() {
        if (mAm!=null){
            mAm.cancel(mPi);
            mAm=null;//變為null
        }
        if (null!=mTag){
            mTag.release();
            //釋放喚醒鎖鎖
            mTag=null;
        }
        mLocationManager.removeUpdates(mLlistener);
        if (mLocationManager != null) {
            mLocationManager.removeUpdates(mLlistener);
            mLocationManager = null;//不用分配空間
        }
        if (mLlistener != null) {
            mLlistener = null;
        }
        //   mLocationManager.setTestProviderEnabled(mProvider, false);
        super.onDestroy();
    }
複製程式碼
  • WakeLock 是為了保持裝置的喚醒狀態的API,組織使用者長時間不用,仍然需要組織裝置進入休眠的狀態,比如使用者在看電影的時候。使用wakelock 時,需要及時的釋放鎖,比如播放視屏的時候WakeLock保持螢幕的常亮,在暫停的時候就應該釋放鎖,而不是等到停止播放才釋放。
 @SuppressLint("WakelockTimeout")
    private void wakeLockDemo() {
//        PowerManager.PARTIAL_WAKE_LOCK;//保持CPU正常運轉,但螢幕和鍵盤燈有可能是關閉的
//        PowerManager.SCREEN_DIM_WAKE_LOCK://保持CPU正常運轉,允許螢幕點亮但可能是置灰的,鍵盤燈可能是關閉的
//        PowerManager.SCREEN_BRIGHT_WAKE_LOCK;//保持CPU正常的運轉,允許螢幕高亮顯示,鍵盤燈可能是關閉的
//        PowerManager.FULL_WAKE_LOCK;//保持CPU正常運轉,保持螢幕高亮顯示,鍵盤燈也保持連讀
//        PowerManager.ACQUIRE_CAUSES_WAKEUP;//強制螢幕和鍵盤燈亮起,這種鎖針對必須通知使用者的操作
//        PowerManager.ON_AFTER_RELEASE;//當WakeLock被釋放了,繼續保持螢幕和鍵盤燈開啟一定的時間
        PowerManager powerManager = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
        /**
         *   case PARTIAL_WAKE_LOCK:
         *             case SCREEN_DIM_WAKE_LOCK:
         *             case SCREEN_BRIGHT_WAKE_LOCK:
         *             case FULL_WAKE_LOCK:
         *             case PROXIMITY_SCREEN_OFF_WAKE_LOCK:
         *             case DOZE_WAKE_LOCK:
         *             case DRAW_WAKE_LOCK:
         */
        mTag = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Tag");
        if (null!= mTag){
            mTag.acquire();
        }

    }

複製程式碼
  • AlarmManager 也是比較耗電的,通常情況下需要保證兩次喚醒操作的時間間隔不要太短了,在不需要使用喚醒功能的情況下,儘早的取消喚醒功能,否則應用會一直消耗電量 AlarmManager 是SDK提供的一個喚醒的APi,是系統級別的服務,可以在特定的時刻廣播一個指定的Intent,這個pendingIntent可以用來啟動Activity、Service、BroadcastReceiver, app,在後臺也會啟動
  private void alarmManager() {
        //建立Intent物件,action為ELITOR_CLOCK,附加資訊為字串“你該打醬油了”
        Intent intent = new Intent("action");
        intent.putExtra("msg","重啟---App ---Le  -- 回到前臺");
//        intent.setClass(ElectricQuantityOptimizationActivity.this,MainActivity.class);
        //定義一個PendingIntent物件,PendingIntent.getBroadcast包含了sendBroadcast的動作。
        //也就是傳送了action 為"action"的intent
        mPi = PendingIntent.getBroadcast(this,0,intent,0);

       //AlarmManager物件,注意這裡並不是new一個物件,Alarmmanager為系統級服務
        mAm = (AlarmManager)getSystemService(ALARM_SERVICE);

       //設定鬧鐘從當前時間開始,每隔5s執行一次PendingIntent物件pi,注意第一個引數與第二個引數的關係
        // 5秒後通過PendingIntent pi物件傳送廣播
        assert mAm != null;
        /**
         * 頻繁的報警對電池壽命不利。至於API 22,警報管理器將覆蓋近期和高頻報警請求,
         * 在未來至少延遲5秒的警報,並確保重複間隔至少為60秒,如果真的需要間隔很短的話,官方建議使用handler
         * 該方法用於設定重複鬧鐘,第一個參數列示鬧鐘型別,第二個參數列示鬧鐘首次執行時間,
         * 第三個參數列示鬧鐘兩次執行的間隔時間,第三個參數列示鬧鐘響應動作。
         */
        mAm.setRepeating(AlarmManager.RTC_WAKEUP,System.currentTimeMillis(),1000, mPi);
        //該方法用於設定一次性鬧鐘,第一個參數列示鬧鐘型別,第二個參數列示鬧鐘執行時間,第三個參數列示鬧鐘響應動作。
        //am.set(AlarmManager.RTC_WAKEUP,100000,pi);
        /**
         * (1)int type: 鬧鐘的型別,常用的有5個值:AlarmManager.ELAPSED_REALTIME、 AlarmManager.ELAPSED_REALTIME_WAKEUP、AlarmManager.RTC、 AlarmManager.RTC_WAKEUP、AlarmManager.POWER_OFF_WAKEUP。AlarmManager.ELAPSED_REALTIME表示鬧鐘在手機睡眠狀態下不可用,該狀態下鬧鐘使用相對時間(相對於系統啟動開始),狀態值為3;
         * AlarmManager.ELAPSED_REALTIME_WAKEUP表示鬧鐘在睡眠狀態下會喚醒系統並執行提示功能,該狀態下鬧鐘也使用相對時間,狀態值為2;
         *
         * AlarmManager.RTC表示鬧鐘在睡眠狀態下不可用,該狀態下鬧鐘使用絕對時間,即當前系統時間,狀態值為1;
         *
         * AlarmManager.RTC_WAKEUP表示鬧鐘在睡眠狀態下會喚醒系統並執行提示功能,該狀態下鬧鐘使用絕對時間,狀態值為0;
         *
         * AlarmManager.POWER_OFF_WAKEUP表示鬧鐘在手機關機狀態下也能正常進行提示功能,所以是5個狀態中用的最多的狀態之一,該狀態下鬧鐘也是用絕對時間,狀態值為4;不過本狀態好像受SDK版本影響,某些版本並不支援;
         */
        //該方法也用於設定重複鬧鐘,與第二個方法相似,不過其兩個鬧鐘執行的間隔時間不是固定的而已。
        //基本上相似,只不過這個方法優化了很多,省電
        // am.setInexactRepeating(AlarmManager.RTC_WAKEUP,System.currentTimeMillis(),1000,pi);
    }


複製程式碼
/**
 * author: Created by shiming on 2018/5/3 14:28
 * mailbox:lamshiming@sina.com
 */
public class MyReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {

        String msg = intent.getStringExtra("msg");
        System.out.println("shiming "  +  msg) ;
        Toast.makeText(context,msg,Toast.LENGTH_SHORT).show();

//        ElectricQuantityOptimizationActivity context1 = (ElectricQuantityOptimizationActivity) context;
//        context1.startActivity(intent);
    }
}
複製程式碼

提供一個Demo,當app崩潰了,通過AlarmManager來重啟App的功能

/**
 * author: Created by shiming on 2018/5/3 14:28
 * mailbox:lamshiming@sina.com
 */
public class CrashHandler implements Thread.UncaughtExceptionHandler {

    public static CrashHandler mAppCrashHandler;

    private Thread.UncaughtExceptionHandler mDefaultHandler;

    private MyApplication mAppContext;

    public void initCrashHandler(MyApplication application) {
        this.mAppContext = application;
        // 獲取系統預設的UncaughtException處理器
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
    }

    public static CrashHandler getInstance() {
        if (mAppCrashHandler == null) {
            mAppCrashHandler = new CrashHandler();
        }
        return mAppCrashHandler;
    }

    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        ex.printStackTrace();
        AlarmManager mgr = (AlarmManager) mAppContext.getSystemService(Context.ALARM_SERVICE);
        Intent intent = new Intent(mAppContext, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        intent.putExtra("crash", true);
        System.out.println("shiming -----》重啟應用了哦");
        PendingIntent restartIntent = PendingIntent.getActivity(mAppContext, 0, intent, PendingIntent.FLAG_ONE_SHOT);
        mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 5000, restartIntent); // 1秒鐘後重啟應用
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(0);
        System.gc();
    }
}

複製程式碼
  • 程式會重新啟動,如果點選電量優化,App崩潰了,請給與全部許可權,還要在開發者模式裡面給與位置資訊模擬的設定,如果崩潰了, 你也可以發現app會自動的重新啟動,這是AlarmManager的應用,注意看MyApplication裡面的程式碼,tks
/**
 * author: Created by shiming on 2018/5/3 14:48
 * mailbox:lamshiming@sina.com
 */

public class MyApplication  extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        /**
         * 程式會重新啟動,如果點選電量優化,App崩潰了,請給與全部許可權,
         * 還要在開發者模式裡面給與位置資訊模擬的設定,如果崩潰了,
         * 你也可以發現app會自動的重新啟動,這是AlarmManager的應用,注意看MyApplication裡面的程式碼,tks
         */
       CrashHandler.getInstance().initCrashHandler(this);
    }
}

複製程式碼

###以上就是個人總結的基本,總結的不太全面,同時也不太詳細,如果可以的話,還請給個小星星,表示鼓勵,謝謝了☺☺☺ ###git地址PerformanceOptimizationForAndroid

相關文章