Android性能優化《Android開發藝術探索》筆記


部分圖片來自Android性能優化:關於 內存泄露 的知識都在這裏了!

Android性能優化

普通優化

1.佈局優化

  • 儘量避免嵌套,可以使用RelativeLayout來取代LinearLayout的多層嵌套,但在使用Relativelayout和LinearLayout都可以的時候,使用LinearLayout。
  • 使用include來複用xml文件
<include layout = "@layout/titlebar">
  • 使用merge來減少佈局的層級
  • 使用ViewStub可以讓佈局在使用時再被加載,提高初始化時的性能。其中id是當前ViewStub的id,而layout則是ViewStub所包含的layout文件,ll_include則是當前layout/include_main_test佈局所對應的根元素的id。
    <ViewStub
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout="@layout/include_main_test"
        android:inflatedId="@id/ll_include"
        android:id="@+id/stud_import"
        />

2.繪製優化

  1. 在onDraw中不要創建局部變量,因爲onDraw會多次調用會佔用內存,同時導致多次GC。
  2. 在onDraw中不要進行耗時操作,也不能進行成千上萬次的循環,這樣會導致View的繪製不流暢。

3.內存泄漏優化

內存泄漏:程序申請的內存,在使用後無法釋放,是導致OOM的主要原因。

內存泄漏的實質無意識地持有對象引用,使得 持有引用者的生命週期 > 被引用者的生命週期

內存溢出:程序在申請內存時,沒有足夠的內存夠其使用

Android內存的管理

在這裏插入圖片描述
進程的內存管理:(內存分配與內存回收)
通過AMS對進程進行內存分配由Framework決定回收的進程類型,當內存緊缺時,會按照優先級的順序對進程進行回收Linux 內核真正回收具體進程。

對象的內存管理與Java虛擬機類似。

存儲包括

方法區中存放已被加載的類信息,以及常量靜態變量
棧幀存放方法執行時的局部變量,包括數據類型對象引用
而對象的實例存於堆中。實例包括對象以及對象所在類的成員變量等等

釋放是對堆中進行GC操作

內存泄漏的實例

1.靜態變量

被 Static 關鍵字修飾的成員變量的生命週期 = 應用程序的生命週期

當前情況下context(成員變量)的生命週期爲應用程序的生命週期大於Activity(引用實例)的生命週期,因此Activity在回收時,由於Context對Activity的持有,因此Activity無法回收。

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    //內存泄漏
    private static Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = this;
    }
}

解決方法
儘量避免靜態成員變量引用資源耗費過多的實例(如 Context)如果需要,可以使用Applaction的Context
context.getApplicationContext()

單例模式的使用

我們需要通過context來創建一個單例的對象(靜態對象,因爲是單例模式),但我們不能直接使用context,而應該通過context.getApplicationContext()來獲取。(不能讓單例的對象一直引用Activity,這樣當Activity準備銷燬時,也會因爲被單例對象引用而無法銷燬,導致內存泄漏)

public class SingleInstanceClass {    
    private static SingleInstanceClass instance;    
    private Context mContext;    
    private SingleInstanceClass(Context context) {        
        this.mContext = context.getApplicationContext(); // 傳遞的是Application 的context
    }    

    public static SingleInstanceClass getInstance(Context context) {        
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }        
        return instance;
    }
}

2.集合類

集合類添加元素後,會對 對象具有引用,即使已經將對象置爲null,也無法回收。

// 通過 循環申請Object 對象 & 將申請的對象逐個放入到集合List
List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
// 雖釋放了集合元素引用的本身:o=null)
// 但集合List 仍然引用該對象,故垃圾回收器GC 依然不可回收該對象

解決方法:在不使用時,必須使用clear方法將所有對象刪除,並將集合類對象置爲null。

 // 釋放objectList
        objectList.clear();
        objectList=null;

3.非靜態內部類/匿名類

3.1創建非靜態內部類的靜態對象

非靜態內部類默認持有外部類的引用,而靜態內部類則不會。

而造成內存泄漏的原因是創建了一個非靜態內部類的靜態對象。則該對象的生命週期=應用的生命週期,因此會在外部類對象銷燬的時候,仍然保留着對外部類的引用,則外部類的實例無法被GC。

public class TestActivity extends AppCompatActivity {  
   
    // 非靜態內部類的實例的引用
    // 注:設置爲靜態  
    public static InnerClass innerClass = null; 
   
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);   

        // 保證非靜態內部類的實例只有1個
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非靜態內部類的定義    
    private class InnerClass {        
        //...
    }
}

解決方法:將內部類改成靜態內部類,這樣就不會有外部類的引用

3.2 匿名類持有外部類的引用

以Thread爲例,我們需要創建一個匿名內部類Thread並實現Runnable接口

工作線程正在處理任務且外部類需銷燬時, 由於工作線程的實例持有外部類引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成內存泄露。

public class MainActivity extends AppCompatActivity {
 @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        ...
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e)
                {
                    e.printStackTrace();
                }
            }
        }).start();

        group.setVisibility(View.GONE);
    }
}

解決方法是可以使用靜態內部類(Thread)

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new MyThread().start();
    }
 private static class MyThread extends Thread{
        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

或者在外部類的回收時,強制關閉線程

 @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部類Activity生命週期結束時,強制結束線程
    }
3.3 Handler

在匿名內部類時,會默認持有外部的引用,也就是說Handler會持有外部類Activity的引用。

  • 當Handler消息隊列 還有未處理的消息 / 正在處理消息時,存在引用關係: “未被處理 / 正處理的消息 -> Handler實例 -> 外部類”(Looper的生命週期與Application相同)

  • 若出現 Handler的生命週期 > 外部類的生命週期 時(即 Handler消息隊列 還有未處理的消息 / 正在處理消息 而 外部類需銷燬時),將使得外部類無法被垃圾回收器(GC)回收,從而造成 內存泄露。

解決方法:靜態內部類+弱引用
靜態內部類不持有外部類的引用,從而使得 “未被處理 / 正處理的消息 -> Handler實例 -> 外部類” 的引用關係 的引用關係 不復存在。而弱引用不論內存是否夠用,在GC時都會被回收

實例如下:

 // 分析1:自定義Handler子類
    // 設置爲:靜態內部類
    private static class FHandler extends Handler{

        // 定義 弱引用實例
        private WeakReference<Activity> reference;

        // 在構造方法中傳入需持有的Activity實例
        public FHandler(Activity activity) {
            // 使用WeakReference弱引用持有Activity實例
            reference = new WeakReference<Activity>(activity); }

        @Override
        public void handleMessage(Message msg) {
           ...
            }
        }
    }

3.4 資源使用未關閉

未及時註銷資源導致內存泄漏,如BraodcastReceiver、File、Cursor、Stream、Bitmap等。

解決辦法:在Activity銷燬的時候要及時關閉或者註銷

BraodcastReceiver:調用unregisterReceiver()註銷;
Cursor,Stream、File:調用close()關閉;
Bitmap:調用recycle()釋放內存(2.3版本後無需手動)
屬性動畫:需要在onDestory中調用Animator.cancel方法來停止無限循環動畫。

3.5 ListView的Adapter導致的內存泄漏

不使用緩存,而一直在getView中重新實例化Item。

解決方法:使用ContertView和ViewHolder,防止多次實例化Item與Item中的子控件。

4ListView優化

主要體現在getView的優化

  1. convertView參數用於將之前加載好的佈局進行緩存
    • 當convertView爲null的時候,首先加載子item佈局,之後創建一個ViewHolder對象,將控件通過findById加載進來,並將控件實例存入ViewHolder對象中。然後調用convertView對象的setTag()方法,將ViewHolder對象存在View中
    • 當convertView不爲null時,就調用convertView對象的getTag()方法,將ViewHolder對象重新取出。這樣所有控件的實例都緩存在了這個ViewHolder中,就不用每次都用findViewById()方法來獲取控件實例。
  2. 在Activity中爲adapter設置listener,就不需要在每次加載getView時重新創建。
  3. 爲控件設置監聽器,同時要向控件傳遞位置(setTag(position)),以便觸發了點擊事件後,可以通過Integer) v.getTag()知道當前點擊的是哪個子項。
   @Override
    public View getView(int position, View convertView, ViewGroup parent){
        ViewHolder viewHolder;
        if (convertView==null) {
            //convertView=View.inflate(mContext, R.layout.series_of_courses_item,null);
            convertView= LayoutInflater.from(mContext).inflate(R.layout.series_of_courses_item,parent,false);
            ViewHolder holder=new ViewHolder();
            holder.course_title=convertView.findViewById(R.id.soc_course_title);
            holder.course_time=convertView.findViewById(R.id.soc_course_time);
            holder.enter_class=convertView.findViewById(R.id.enter_class);
            holder.btn_son_course_delete = convertView.findViewById(R.id.btn_son_course_delete);
            convertView.setTag(holder);
        }
        viewHolder=(ViewHolder)convertView.getTag();
        final SeriesCourseModel seriesCourses=data.get(position);
        //Log.d("SeriesCoursesAdapter", "getView: " + courses.getCourseName());
        viewHolder.course_title.setText(seriesCourses.getName());
        //int minutes= (int) LangUtils.parseLong(seriesCourses.getCourseTime(),0)/60;
        viewHolder.course_time.setText(seriesCourses.getCourseTime());
        //註冊Item控件點擊事件,首先在Activity中要設置監聽器
        if (onClickListener != null){
            viewHolder.enter_class.setOnClickListener(onClickListener);
            viewHolder.enter_class.setTag(position);
            viewHolder.btn_son_course_delete.setOnClickListener(onClickListener);
            viewHolder.btn_son_course_delete.setTag(position);
        }
        //type=1就顯示那個button
        if (type==1) {
            viewHolder.enter_class.setVisibility(View.VISIBLE);
        }
        else viewHolder.enter_class.setVisibility(View.INVISIBLE);
        return convertView;
    }
    class ViewHolder{
        TextView course_title;
        TextView course_time;
        Button enter_class;
        ImageButton btn_son_course_delete;
    }
     //向XListView添加數據項
    public void addItems(SeriesCourseModel seriesCourseModel){
        this.data.add(seriesCourseModel);
        notifyDataSetChanged();
    }
    //向XListView刪除數據項 注意添加了headView
    public void deleteItems(int position){
        this.data.remove(position);
        notifyDataSetChanged();
    }

5.BitMap優化

Bitmap的高效加載

  1. 將BitmapFactory.Options.inJustDecodeBounds參數設爲true並加載圖片。
  2. 從BitmapFactory.Options中取出圖片的原始寬高信息decodeResource(res,resId,options),對應outWidth和outHeight參數。
  3. 根據採樣率的規則並結合目標View的所需大小計算出採樣率inSampleSize
  4. 將BitmapFactory.Options.inJustDecodeBounds參數設爲false,然後重新加載圖片decodeResource(res,resId,options)
  /**
     * 對一個Resources的資源文件進行指定長寬來加載進內存, 並把這個bitmap對象返回
     *
     * @param res   資源文件對象
     * @param resId 要操作的圖片id
     * @param reqWidth 最終想要得到bitmap的寬度
     * @param reqHeight 最終想要得到bitmap的高度
     * @return 返回採樣之後的bitmap對象
     */
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight){
        BitmapFactory.Options options = new BitmapFactory.Options();
        //1.設置inJustDecodeBounds=true獲取圖片尺寸
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res,resId,options);
        //3.計算縮放比
        options.inSampleSize = calculateInSampleSize(options,reqHeight,reqWidth);
        //4.再設爲false,重新從資源文件中加載圖片
        options.inJustDecodeBounds =false;
        return BitmapFactory.decodeResource(res,resId,options);
    }

   /**
     *  一個計算工具類的方法, 傳入圖片的屬性對象和想要實現的目標寬高. 通過計算得到採樣值
     * @param options 要操作的原始圖片屬性
     * @param reqWidth 最終想要得到bitmap的寬度
     * @param reqHeight 最終想要得到bitmap的高度
     * @return 返回採樣率
     */
    private static int calculateInSampleSize(BitmapFactory.Options options, int reqHeight, int reqWidth) {
        //2.height、width爲圖片的原始寬高
        int height = options.outHeight;
        int width = options.outWidth;
        int inSampleSize = 1;
        if(height>reqHeight||width>reqWidth){
            int halfHeight = height/2;
            int halfWidth = width/2;
            //計算縮放比,是2的指數
            while((halfHeight/inSampleSize)>=reqHeight&&(halfWidth/inSampleSize)>=reqWidth){
                inSampleSize*=2;
            }
        }    
        return inSampleSize;
    }

Bitmap的緩存策略

當緩存滿時, 會優先淘汰那些近期最少使用的緩存對象。

LruCache類是一個線程安全的泛型類:內部採用一個LinkedHashMap以強引用的方式存儲外界的緩存對象,並提供get和put方法來完成緩存的獲取和添加操作,當緩存滿時會移除較早使用的緩存對象,再添加新的緩存對象。

常用屬性accessOrder:決定LinkedHashMap的鏈表順序。值爲true:以訪問順序維護鏈表。值爲false:以插入的順序維護鏈表

而LruCache利用是accessOrder=true 時的LinkedHashMap實現LRU算法,使得最近訪問的數據會在鏈表尾部,在容量溢出時,將鏈表頭部的數據移除
在這裏插入圖片描述
使用方法
計算當前可用的內存大小;
分配LruCache緩存容量;
創建LruCache對象並傳入最大緩存大小的參數、重寫sizeOf()用於計算每個緩存對象的大小;
通過put()、get()和remove()實現數據的添加、獲取和刪除

 lruCache = new LruCache<String, Bitmap>(maxSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                return value.getWidth() * value.getHeight() / 1024;
            }
        };

6.線程優化

使用線程池,避免創建大量的Thread。Android線程與線程池《開發藝術》探索

7.響應速度優化

避免在主線程中做耗時操作。Activity規定如果在5s之內無法響應點擊事件或鍵盤輸入事件就會ANR,如果10s內BroadCastReciver還沒有完成執行邏輯就會ANR。當發生ANR後可以通過traces文件定位。

8.其他建議

  1. 避免創建過多對象
  2. 不要過多使用枚舉,枚舉的佔用的內存空間比整形大
  3. 常量要使用static final來修飾
  4. 使用一些Android自帶的數據結構,比如Pair,他們具有更好的性能,Pair可以理解爲一個帶有xy座標的Point類。List<Pair<Integer,Integer>>list = new ArrayList<>();
  5. 適當使用軟引用與弱引用(弱引用可以防止內存泄漏)。
  6. 採用內存緩存與磁盤緩存
  7. 儘量使用靜態內部類,防止非靜態內部類具有外部類的引用而導致內存泄漏
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章