Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android性能優化與內存洩漏分析

Android性能優化與內存洩漏分析

編輯:關於Android編程

性能問題一般歸結為3類:

1.UI卡頓和穩定性,這類問題用戶可直接感知,最為重要。

2.內存問題,內存問題主要表現為內存洩漏。如果存在內存洩漏,應用會不斷消耗內存,

容易導致頻繁GC使系統出現卡頓(內存不足時會調用垃圾回收機制回收內存,GC比較消耗性能),

或者出現OOM報錯。

3.耗電問題,會影響續航,表現為不必要的自啟動,不恰當持鎖導致系統無法正常休眠,系統休眠後頻繁

喚醒系統等。

一. UI卡頓常見原因和分析方法

1.人為在UI線程中做輕微耗時操作,導致UI線程卡頓。

2.布局Layout過於復雜,無法在16ms內完成渲染。

3.同一時間動畫執行的次數過多,導致CPU或GPU負載過重。

4.View過度繪制,導致某些像素在同一幀時間內被繪制多次,從而使CPU或GPU負載過重;

5.View頻繁的觸發measure、layout,導致measure、layout累計耗時過多及整個View頻繁的重新渲染;

6.內存頻繁觸發GC過多(同一幀中頻繁創建內存),導致暫時阻塞渲染操作;

7.冗余資源及邏輯等導致加載和執行緩慢; 8.工作線程優先級未設置為 Process.THREAD_PRIORITY_BACKGROUND 導致後台線程搶占UI線程cpu時間片,阻塞渲染操作; 9.ANR;

二. 卡頓分析解決的一般步驟

1)解決過度繪制問題

>在設置開發者選項調試GPU過度繪制中打開調試,看對應界面是否有過度繪制,如果有先解決掉:

123456.png

> 定位過渡繪制區域

> 利用Android提供的工具進行位置確認以及修改(HierarchyView , Tracer for OpenGL ES)

> 定位到具體的視圖(xml文件或者View)

> 通過代碼和xml文件分析過渡繪制的原因

> 結合具體情況進行優化

> 使用Lint工具進一步優化

2) 檢查是否有主線程做了耗時操作:

嚴苛模式(StrictMode),是Android提供的一種運行時檢測機制,用於檢測代碼運行時的一些不規范的操作,最常見的場景是用於發現主線程的IO操作。應用程序可以利用StrictMode盡可能的發現一些編碼的疏漏。

> 開啟 StrictMode

>> 對於應用程序而言,Android 提供了一個最佳使用實踐:盡可能早的在

android.app.Application 或 android.app.Activity 的生命周期使能 StrictMode,onCreate()方法就是一個最佳的時機,越早開啟就能在更多的代碼執行路徑上發現違規操作。

>> 監控代碼

publicvoidonCreate(){
if(DEVELOPER_MODE){
StrictMode.setThreadPolicy(newStrictMode.ThreadPolicy.Builder()
.detectAll().penaltyLog().build());
StrictMode.setVmPolicy(newStrictMode.VmPolicy.Builder()
.detectAll().penaltyLog().build());
}
super.onCreate();
}

如果主線程有網絡或磁盤讀寫等操作,在logcat中會有"D/StrictMode"tag的日志輸出,從而定位到耗時操作的代碼。

3)如果主線程無耗時操作,還存在卡頓,有很大可能是必須在UI線程操作的一些邏輯有問題,比如控件measure、layout耗時過多等,此時可通過Traceview以及systrace來進行分析。

4)Traceview:Traceview主要用做熱點分析,找出最需要優化的點。

> 打開DDMS然後選擇一個進程,接著點擊上面的“Start Method Profiling”按鈕(紅色小點變為黑色即開始運行),然後操作我們的卡頓UI,然後點擊"Stop Method Profiling",會打開如下界面:

111.png

圖中展示了Trace期間各方法調用關系,調用次數以及耗時比例。通過分析可以找出可疑的耗時函數並進行優化;

5)systrace:抓取trace:

> 執行如下命令:

$cdandroid-sdk/platform-tools/systrace
$pythonsystrace.py--time=10-omynewtrace.htmlschedgfxviewwm

> 操作APP,然後會生成一個mynewtrace.html 文件,用Chrome打開。

> 圖示如下:

1231.png

通過分析上面的圖,可以找出明顯存在的layout,measure,draw的超時問題。

6)導入如下插件,可通過在方法上添加@DebugLog來打印方法的耗時:

build.gradle:
buildscript{
dependencies{

//用於方便調試性能問題的打印插件。給訪法加上@DebugLog,就能輸出該方法的調用參數,以及執行時間;

class path com.jakewharton.hugo:hugo-plugin:1.2.1
}
}

//用於方便調試性能問題的打印插件。給訪法加上@DebugLog,就能輸出該方法的調用參數,以及執行時間;

apply plugin:com.jakewharton.hugo
java:
@Debug Log
public void test(int a){
int b=a*a;
}
三.內存性能優化分析(內存洩漏問題)

Java是垃圾回收語言的一種,其優點是開發者無需特意管理內存分配,降低了應用由於局部故障(segmentation fault)導致崩潰,同時防止未釋放的內存把堆棧(heap)擠爆的可能,所以寫出來的代碼更為安全。

不幸的是,在Java中仍存在很多容易導致內存洩漏的邏輯可能(logical leak)。如果不小心,你的Android應用很容易浪費掉未釋放的內存,最終導致內存用光的錯誤拋出(out-of-memory,OOM)。

一般內存洩漏(traditional memory leak)的原因是:當該對象的所有引用都已經釋放了,對象仍未被釋放。(譯者注:Cursor忘記關閉等)
邏輯內存洩漏(logical memory leak)的原因是:當應用不再需要這個對象,當仍未釋放該對象的所有引用。

如果持有對象的強引用,垃圾回收器是無法在內存中回收這個對象。

在Android開發中,最容易引發的內存洩漏問題的是Context。比如Activity的Context,就包含大量的內存引用,例如View Hierarchies和其他資源。一旦洩漏了Context,也意味洩漏它指向的所有對象。Android機器內存有限,太多的內存洩漏容易導致OOM。

檢測邏輯內存洩漏需要主觀判斷,特別是對象的生命周期並不清晰。幸運的是,Activity有著明確的生命周期,很容易發現洩漏的原因。Activity.onDestroy()被視為Activity生命的結束,程序上來看,它應該被銷毀了,或者Android系統需要回收這些內存(譯者注:當內存不夠時,Android會回收看不見的Activity)。
如果這個方法執行完,在堆棧中仍存在持有該Activity的強引用,垃圾回收器就無法把它標記成已回收的內存,而我們本來目的就是要回收它!
結果就是Activity存活在它的生命周期之外。

Activity是重量級對象,應該讓Android系統來處理它。然而,邏輯內存洩漏總是在不經意間發生。(譯者注:曾經試過一個Activity導致20M內存洩漏)。在Android中,導致潛在內存洩漏的陷阱不外乎兩種:

  • 全局進程(process-global)的static變量。這個無視應用的狀態,持有Activity的強引用的怪物。
  • 活在Activity生命周期之外的線程。沒有清空對Activity的強引用。

檢查一下你有沒有遇到下列的情況。

Static Activities

在類中定義了靜態Activity變量,把當前運行的Activity實例賦值於這個靜態變量。
如果這個靜態變量在Activity生命周期結束後沒有清空,就導致內存洩漏。因為static變量是貫穿這個應用的生命周期的,所以被洩漏的Activity就會一直存在於應用的進程中,不會被垃圾回收器回收。

     static Activity activity;

    void setStaticActivity() {
      activity = this;
    }

    View saButton = findViewById(R.id.sa_button);
    saButton.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        setStaticActivity();
        nextActivity();
      }
    });

\

Memory Leak 1 – Static Activity

Static Views

類似的情況會發生在單例模式中,如果Activity經常被用到,那麼在內存中保存一個實例是很實用的。正如之前所述,強制延長Activity的生命周期是相當危險而且不必要的,無論如何都不能這樣做。

特殊情況:如果一個View初始化耗費大量資源,而且在一個Activity生命周期內保持不變,那可以把它變成static,加載到視圖樹上(View Hierachy),像這樣,當Activity被銷毀時,應當釋放資源。(譯者注:示例代碼中並沒有釋放內存,把這個static view置null即可,但是還是不建議用這個static view的方法)

    static view;

    void setStaticView() {
      view = findViewById(R.id.sv_button);
    }

    View svButton = findViewById(R.id.sv_button);
    svButton.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
        setStaticView();
        nextActivity();
      }
    });

\

Memory Leak 2 – Static View

Inner Classes

繼續,假設Activity中有個內部類,這樣做可以提高可讀性和封裝性。將如我們創建一個內部類,而且持有一個靜態變量的引用,恭喜,內存洩漏就離你不遠了(譯者注:銷毀的時候置空,嗯)。

       private static Object inner;

       void createInnerClass() {
        class InnerClass {
        }
        inner = new InnerClass();
    }

    View icButton = findViewById(R.id.ic_button);
    icButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            createInnerClass();
            nextActivity();
        }
    });

\

Memory Leak 3 – Inner Class

內部類的優勢之一就是可以訪問外部類,不幸的是,導致內存洩漏的原因,就是內部類持有外部類實例的強引用。

Anonymous Classes

相似地,匿名類也維護了外部類的引用。所以內存洩漏很容易發生,當你在Activity中定義了匿名的AsyncTsk
。當異步任務在後台執行耗時任務期間,Activity不幸被銷毀了(譯者注:用戶退出,系統回收),這個被AsyncTask持有的Activity實例就不會被垃圾回收器回收,直到異步任務結束。

    void startAsyncTask() {
        new AsyncTask() {
            @Override protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
    }

    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    View aicButton = findViewById(R.id.at_button);
    aicButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            startAsyncTask();
            nextActivity();
        }
    });

\

Memory Leak 4 – AsyncTask

Handler

同樣道理,定義匿名的Runnable,用匿名類Handler執行。Runnable內部類會持有外部類的隱式引用,被傳遞到Handler的消息隊列MessageQueue中,在Message消息沒有被處理之前,Activity實例不會被銷毀了,於是導致內存洩漏。

    void createHandler() {
        new Handler() {
            @Override public void handleMessage(Message message) {
                super.handleMessage(message);
            }
        }.postDelayed(new Runnable() {
            @Override public void run() {
                while(true);
            }
        }, Long.MAX_VALUE >> 1);
    }

    View hButton = findViewById(R.id.h_button);
    hButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            createHandler();
            nextActivity();
        }
    });

\

Memory Leak 5 – Handler

Threads

我們再次通過Thread和TimerTask來展現內存洩漏。

    void spawnThread() {
        new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
    }

    View tButton = findViewById(R.id.t_button);
    tButton.setOnClickListener(new View.OnClickListener() {
      @Override public void onClick(View v) {
          spawnThread();
          nextActivity();
      }
    });

\

Memory Leak 6 – Thread

TimerTask

只要是匿名類的實例,不管是不是在工作線程,都會持有Activity的引用,導致內存洩漏。

    void scheduleTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                while(true);
            }
        }, Long.MAX_VALUE >> 1);
    }

    View ttButton = findViewById(R.id.tt_button);
    ttButton.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            scheduleTimer();
            nextActivity();
        }
    });

\

Memory Leak 7 – TimerTask

Sensor Manager

最後,通過Context.getSystemService(int name)可以獲取系統服務。這些服務工作在各自的進程中,幫助應用處理後台任務,處理硬件交互。如果需要使用這些服務,可以注冊監聽器,這會導致服務持有了Context的引用,如果在Activity銷毀的時候沒有注銷這些監聽器,會導致內存洩漏。

        void registerListener() {
               SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
               Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
               sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
        }

        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                registerListener();
                nextActivity();
            }
        });

\

Memory Leak 8 – Sensor Manager

看過那麼多會導致內存洩漏的例子,容易導致吃光手機的內存使垃圾回收處理更為頻發,甚至最壞的情況會導致OOM。垃圾回收的操作是很昂貴的開銷,會導致肉眼可見的卡頓。所以,實例化的時候注意持有的引用鏈,並經常進行內存洩漏檢查。

四、耗電量優化建議

電量優化主要是注意盡量不要影響手機進入休眠,也就是正確申請和釋放WakeLock,另外就是不要頻繁喚醒手機,主要就是正確使用Alarm。

五、一些好的代碼實踐

1. 節制地使用Service

2. 當界面不可見時釋放內存

3. 當內存緊張時釋放內存

4. 避免在Bitmap上浪費內存

對大圖片,先獲取圖片的大小信息,根據實際需要展示大小計算inSampleSize,最後decode;

public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth,int reqHeight){
//First decodewithinJustDecodeBounds=truetocheckdimensions
final BitmapFactory.Optionsoptions=new BitmapFactory.Options();
options.inJustDecodeBounds=true;
BitmapFactory.decodeFile(filename,options);
//CalculateinSampleSize
options.inSampleSize=
reqHeight);
calculateInSampleSize(options,
reqWidth,reHeight);
//DecodebitmapwithinSampleSizeset
options.inJustDecodeBounds=false;
return BitmapFactory.decodeFile(filename,options);
}
public static int calculateInSampleSize(BitmapFactory.Optionsoptions,
intreqWidth,intreqHeight){
//Rawheightandwidthofimage
final int height=options.outHeight;
final int width=options.outWidth;
int inSampleSize=1;
if(height>reqHeight||width>reqWidth){
if(width>height){
inSampleSize=Math.round((float)height/(float)reqHeight);
}else{
inSampleSize=Math.round((float)width/(float)reqWidth);
}
}
return inSampleSize;
}

5. 使用優化過的數據集合

6. 謹慎使用抽象編程

7. 盡量避免使用依賴注入框架

很多依賴注入框架是基於反射的原理,雖然可以讓代碼看起來簡潔,但是是有礙性能的。

8. 謹慎使用external libraries

9. 優化整體性能

10. 使用ProGuard來剔除不需要的代碼

android{
buildTypes{
release{
minifyEnabledtrue
shrinkResources true
proguardFilesgetDefaultProguardFile(proguard-android.txt),src/main/proguard-project.txt
signingConfigsigningConfigs.debug
}
}

11. 慎用異常,異常對性能不利

拋出異常首先要創建一個新的對象。Throwable 接口的構造函數用名為

fillInStackTrace() 的本地方法,fillInStackTrace() 方法檢查棧,收集調用跟蹤信

息。只要有異常被拋出,VM 就必要調整調用棧,因為在處理過程中創建了一

個新對象。

異常只能用於錯誤處理,不應該用來控制程序流程。

以下例子不好:

try{
startActivity(intentA);
}catch(){
startActivity(intentB);
}

應該用下面的語句判斷:

if(getPackageManager().resolveActivity(intentA,0)!=null)

不要再循環中使用 try/catch 語句,應把其放在最外層,使用 System.arraycopy()代替 for 循環復制。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved