Android 內存優化最少必要知識

1. 爲何要進行內存優化?

Android 系統爲每個 App 分配的內存都是有限制的,一旦 App 使用的內存超限以後,將致使 OOM(Out of Memory),即 App 異常退出。html

2. 如何查看一個 App 的內存限制?

ActivityManager activityManager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
//1. 當前設備上,系統爲每一個 App 分配的內存空間的近似值,單位是 M。
int memory = activityManager.getMemoryClass();
//2. 當前設備上,系統爲該 App 分配的最大內存空間的近似值,單位是 M。
int largeMemory = activityManager.getLargeMemoryClass();
Log.e(Constants.TAG, "Memory: " + memory + " LargeMemory: " + largeMemory);

//執行結果:
2019-09-19 17:39:44.792 17771-17771/com.smart.a17_memory_optimization E/TAG: Memory:  256  LargeMemory:  512
複製代碼

一般狀況下,memory 和 largeMemory 的值是同樣的,只有在清單文件中專門設置了「android:largeHeap="true"」屬性時,兩者的值纔不同。android

//在清單文件中設置 android:largeHeap 屬性爲 true
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:largeHeap="true"
    android:theme="@style/AppTheme">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>
複製代碼

不過,大多數狀況下不須要這樣專門設置,由於系統默認分配的內存已經夠用了。若是手機上全部的 App 都申請了最大內存,那最終全部的 App 都申請不到最大內存空間。緩存

3. Android 中內存是如何分配的?

  • 堆(Heap):存儲 new 出來的對象的實體,由 GC 回收;
  • 棧(Stack):存儲方法中的變量,方法結束後自動釋放內存;
  • 方法區:在方法區內包含常量池和靜態成員變量,其中常量池存放的是常量,靜態成員變量區存放的是靜態成員變量,編譯時已分配好,在程序的整個運行期間都存在;
public class Teacher {
    
    private int age = 24; //堆
    
    private static String country; //靜態變量區
    
    private final int luckyNumber = 7; //常量池
    
    private Student student = new Student(); //堆

    public void askQuestion(){
        
        int number = 2333333; //棧
       
        Student student = new Student(); //student 棧,new Student() 堆
        
    }

}
複製代碼

4. 如何檢測 App 內存消耗?如何檢測內存泄漏?

經常使用的內存檢測工具備:性能優化

  • Android Profiler
  • MAT(Memory Analyzer Tool)

經常使用的內存泄漏檢測工具備:bash

  • LeakCanary

4.1 Android Profiler

Android Profiler 是 Android Studio 自帶的用於檢測 CPU、內存、流量、電量使用狀況的工具。網絡

它的特色是能直觀地展現當前 App 的 CPU、內存、流量、電量使用狀況。併發

打開的方法:app

View → Tool Windows → Profiler  
複製代碼

或者直接點擊下面這裏:dom

介於篇幅,我就不在此贅述如何使用 Android Profiler 了,想要了解關於 Android Profiler 的使用方法,請參考 Google 開發者文檔ide

4.2 MAT(Memory Analyzer Tool)

同上,這個工具我也很少說了,你們自行學習相關知識吧。

4.3 LeakCanary

LeakCanary 出自 Square 公司,它主要用於檢測內存泄漏,使用也十分簡單。

  1. 在 build.gradle 中添加依賴;
  2. 在 Application 中註冊;

That's all,當軟件發生內存泄漏的時候,它會提示開發者。

接下來,咱們就手動寫一個內存泄漏的例子,看下 LeakCanary 能不能檢測出來:

//1. 在 build.gradle 中添加依賴
apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"


    defaultConfig {
        applicationId "com.smart.a17_memory_optimization"
        minSdkVersion 14
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'

    //LeakCanary 依賴
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
}


//2. 在 Application 中註冊
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        initData();
    }

    private void initData(){
        LeakCanary.install(this);
    }

}

//3. MainActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mJumpToLeakMemoryDemo;

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

    private void initView() {
        mJumpToLeakMemoryDemo = findViewById(R.id.jump_to_memory_leak_demo);
        mJumpToLeakMemoryDemo.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        int id = view.getId();
        switch (id) {
            case R.id.jump_to_memory_leak_demo:
                ActivityUtils.startActivity(this, MemoryLeakDemoActivity.class);
                break;
        }
    }

}

//4. MemoryLeakDemoActivity
public class MemoryLeakDemoActivity extends AppCompatActivity {

    private boolean mIsNeedStop = false;
    private static CustomThread mCustomThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_demo);
        initData();
    }

    private void initData() {
        mCustomThread = new CustomThread();
        mCustomThread.start();
    }

    class CustomThread extends Thread{
        @Override
        public void run() {
            super.run();
            while (!mIsNeedStop){
                try {
                    Log.e(Constants.TAG, String.valueOf(System.currentTimeMillis()));
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mIsNeedStop = true;
    }

}

複製代碼

最終效果以下:

5. 常見的內存性能問題都有哪些?如何檢測?如何解決?

常見的內存性能問題有:

  1. 內存泄漏;
  2. 內存抖動;
  3. 圖片優化;
  4. 其餘優化;

5.1 內存泄漏

形成內存泄漏的緣由仍是挺多的,常見的有:

  1. 單例(Context 使用不當);
  2. 建立非靜態內部類的靜態實例;
  3. Handler 未及時解除關聯的 Message;
  4. 線程;
  5. 資源未關閉;

5.1.1 單例(Context 使用不當)

//1. 自定義 ToastUtils
public class ToastUtils {

    private static ToastUtils mInstance;
    private static Context mContext;
    private static Toast mToast;

    private ToastUtils(Context ctx){
        this.mContext = ctx;
    }

    public static ToastUtils getInstance(Context ctx){
        if(mInstance == null){
            mInstance = new ToastUtils(ctx);
            if(Constants.IS_DEBUG){
                Log.e(Constants.TAG, "ToastUtils is null");
            }
        }
        return mInstance;
    }

    public static void showToast(String msg){
        if(mContext == null){
            throw new NullPointerException("Context can't be null");
        }
        if(mToast == null){
            mToast = Toast.makeText(mContext, msg, Toast.LENGTH_SHORT);
        }
        mToast.setText(msg);
        mToast.show();
    }

}

//2. MemoryLeakSingletonActivity(應用 ToastUtils) 
public class MemoryLeakSingletonActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mShowToast;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_singleton);
        initView();
    }

    private void initView(){
        mShowToast = findViewById(R.id.show_toast);
        mShowToast.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        ToastUtils.getInstance(this).showToast(getResources().getString(R.string.show_toast));
    }

}
複製代碼

MemoryLeakSingletonActivity 界面以下:

當點擊 ShowToast 按鈕並退出 MemoryLeakSingletonActivity 以後,收到的 LeakCanary 發出的內存泄漏通知:

形成問題的緣由:

  1. 概述:

    ToastUtils 和 MemoryLeakSingletonActivity 生命週期不一致致使的內存泄漏。

  2. 詳述:

    ToastUtils 是一個單例類,也就是說,一旦 ToastUtils 建立成功以後,ToastUtils 生命週期將和 App 生命週期同樣長,而它所引用的 Activity 的生命週期卻沒有那麼長,當用戶從該 Activity 退出以後,因爲 ToastUtils 依然持有 Activity 的引用,因此,致使 Activity 沒法釋放,形成內存泄漏。

  3. 解決方法

    將 ToastUtils 中使用的 Activity(Context)改成 Application Context,因爲 Application Context 與 App 生命週期相同,因此,不存在內存泄漏問題,即解決掉了上面的問題。

//修改以後的 ToastUtils
public class ToastUtils {

    private static ToastUtils mInstance;
    private static Context mContext;
    private static Toast mToast;

    private ToastUtils(Context ctx){

        //1. ToastUtils 與 Context 生命週期不一樣,內存泄漏
        //this.mContext = ctx;

        //2. ToastUtils 與 Context 生命週期相同,不存在內存泄漏
        this.mContext = ctx.getApplicationContext();
    }

    public static ToastUtils getInstance(Context ctx){
        if(mInstance == null){
            mInstance = new ToastUtils(ctx);
            if(Constants.IS_DEBUG){
                Log.e(Constants.TAG, "ToastUtils is null");
            }
        }
        return mInstance;
    }

    public static void showToast(String msg){
        if(mContext == null){
            throw new NullPointerException("Context can't be null");
        }
        if(mToast == null){
            mToast = Toast.makeText(mContext, msg, Toast.LENGTH_SHORT);
        }
        mToast.setText(msg);
        mToast.show();
    }

}
複製代碼

5.1.2 建立非靜態內部類的靜態實例

//1. MemoryLeakStaticInstanceActivity
public class MemoryLeakStaticInstanceActivity extends AppCompatActivity {

    private boolean mIsNeedStop = false;
    private static CustomThread mCustomThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_demo);
        initData();
    }

    private void initData() {
        mCustomThread = new CustomThread();
        mCustomThread.start();
    }

    class CustomThread extends Thread{
        @Override
        public void run() {
            super.run();
            while (!mIsNeedStop){
                try {
                    Log.e(Constants.TAG, String.valueOf(System.currentTimeMillis()));
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mIsNeedStop = true;
    }
    
}
複製代碼

當退出 MemoryLeakStaticInstanceActivity 界面以後,收到的 LeakCanary 發出的內存泄漏通知:

形成問題的緣由:

  1. 概述:

    CustomThread 和 MemoryLeakStaticInstanceActivity 生命週期不一致致使的內存泄漏。

  2. 詳述:

    非靜態內部類 CustomThread 持有外部類 MemoryLeakStaticInstanceActivity 的引用,因爲在聲明 CustomThread 時使用了靜態變量,即 CustomThread 實例與 App 生命週期同樣長,而它所引用的 Activity 的生命週期卻沒有那麼長,當用戶從該 Activity 退出以後,因爲 CustomThread 依然持有 Activity 的引用,因此,致使 Activity 沒法釋放。

  3. 解決方法

    • 聲明 CustomThread 變量時,去掉 static 關鍵字,即把 CustomThread 聲明爲非靜態,且退出時,關閉 CustomThread;
    • 把 CustomThread 類提取爲一個單獨的類或者將 CustomThread 類聲明爲靜態內部類,且退出時,關閉 CustomThread;
//1. 聲明 CustomThread 變量時,去掉 static 關鍵字,即把 CustomThread 聲明爲非靜態,且退出時,關閉 CustomThread
public class MemoryLeakStaticInstanceActivity extends AppCompatActivity {

    private static boolean mIsNeedStop = false;
    private CustomThread mCustomThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_demo);
        initData();
    }

    private void initData() {
        mCustomThread = new CustomThread();
        mCustomThread.start();
    }

    class CustomThread extends Thread{
        @Override
        public void run() {
            super.run();
            while (!mIsNeedStop){
                try {
                    Log.e(Constants.TAG, String.valueOf(System.currentTimeMillis()));
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mIsNeedStop = true;
    }
    
}

//2. 把 CustomThread 類提取爲一個單獨的類或者將 CustomThread 類聲明爲靜態內部類,且退出時,關閉 CustomThread
public class MemoryLeakStaticInstanceActivity extends AppCompatActivity {

    private static boolean mIsNeedStop = false;
    private static CustomThread mCustomThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_demo);
        initData();
    }

    private void initData() {
        mCustomThread = new CustomThread();
        mCustomThread.start();
    }

    static class CustomThread extends Thread{
        @Override
        public void run() {
            super.run();
            while (!mIsNeedStop){
                try {
                    Log.e(Constants.TAG, String.valueOf(System.currentTimeMillis()));
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mIsNeedStop = true;
    }
    
}
複製代碼

5.1.3 Handler 未及時解除關聯的 Message

//1. MemoryLeakHandlerActivity
public class MemoryLeakHandlerActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mContent;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if(Constants.IS_DEBUG){
                Log.e(Constants.TAG, "Handler handleMessage");
                Message message = new Message();
                message.obj = getResources().getString(R.string.spending_traffic);
                mHandler.sendMessageDelayed(message, 1000 * 1);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_handler);
        initView();
    }

    private void initView() {
        mContent = findViewById(R.id.content);
        mContent.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        Message message = new Message();
        message.obj = getResources().getString(R.string.spending_traffic);
        mHandler.sendMessageDelayed(message, 1000 * 1);
    }

}
複製代碼

當退出 MemoryLeakHandlerActivity 界面以後,收到的 LeakCanary 發出的內存泄漏通知:

形成問題的緣由:

  1. 概述:

    MemoryLeakHandlerActivity 結束的時候,MessageQueue 中有待處理的 Message。

  2. 詳述:

    非靜態內部類 Handler 持有外部類 MemoryLeakHandlerActivity 的引用,因爲在 Handler 的 handleMessage() 方法的內部,Handler 又給本身發送了消息,因此,即使是 MemoryLeakHandlerActivity 退出了,但它(MemoryLeakHandlerActivity)依然沒法釋放,由於 Handler 一直在運行且持有對 MemoryLeakHandlerActivity 的引用。

  3. 解決方法

    • 將 Handler 聲明爲靜態;
    • 在 Activity 退出的時候,經過調用 Handler.removeCallbacksAndMessages() 方法將 MessageQueue 中的 Message 移除;
//1. 將 Handler 聲明爲靜態
public class MemoryLeakHandlerActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mContent;
    private static Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if(Constants.IS_DEBUG){
                Log.e(Constants.TAG, "Handler handleMessage");
                Message message = new Message();
                message.obj = "Message";
                mHandler.sendMessageDelayed(message, 1000 * 1);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_handler);
        initView();
    }

    private void initView() {
        mContent = findViewById(R.id.content);
        mContent.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        Message message = new Message();
        message.obj = getResources().getString(R.string.spending_traffic);
        mHandler.sendMessageDelayed(message, 1000 * 1);
    }

}

//2. 在 Activity 退出的時候,經過調用 Handler.removeCallbacksAndMessages() 方法將 MessageQueue 中的 Message 移除
public class MemoryLeakHandlerActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mContent;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            if(Constants.IS_DEBUG){
                Log.e(Constants.TAG, "Handler handleMessage");
                Message message = new Message();
                message.obj = getResources().getString(R.string.spending_traffic);
                mHandler.sendMessageDelayed(message, 1000 * 1);
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_handler);
        initView();
    }

    private void initView() {
        mContent = findViewById(R.id.content);
        mContent.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        Message message = new Message();
        message.obj = getResources().getString(R.string.spending_traffic);
        mHandler.sendMessageDelayed(message, 1000 * 1);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //2. 在 Activity 退出的時候,經過調用 Handler.removeCallbacksAndMessages() 方法將 MessageQueue
        //中的 Message 移除
        mHandler.removeCallbacksAndMessages(null);
    }

}
複製代碼

5.1.4 線程未解除與其關聯的 Activity

//1. MemoryLeakThreadActivity
public class MemoryLeakThreadActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mDoBackgroundTask;
    private static boolean isNeedStop = false;
    private CustomRunnable mCustomRunnable;
    private Thread mThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_thread);
        initView();
    }

    private void initView() {
        mDoBackgroundTask = findViewById(R.id.do_background_task);
        mDoBackgroundTask.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(mThread == null){
            isNeedStop = false;
            mCustomRunnable = new CustomRunnable();
            mThread = new Thread(mCustomRunnable);
            mThread.start();
        }else{
            Toast.makeText(this, "任務已經在運行", Toast.LENGTH_SHORT).show();
        }
    }

    class CustomRunnable implements Runnable{

        @Override
        public void run() {
            while (!isNeedStop){
                try {
                    if(Constants.IS_DEBUG){
                        Log.e(Constants.TAG, "CUSTOM RUNNABLE RUNNING");
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
複製代碼

當退出 MemoryLeakThreadActivity 界面以後,收到的 LeakCanary 發出的內存泄漏通知:

形成問題的緣由:

  1. 概述:

    MemoryLeakHandlerActivity 結束的時候,CustomRunnable 仍在運行,且 CustomRunnable 中有 MemoryLeakThreadActivity 的引用,故致使 MemoryLeakHandlerActivity 不能釋放。

  2. 詳述:

    非靜態內部類 CustomRunnable 持有外部類 MemoryLeakThreadActivity 的引用,因爲 MemoryLeakThreadActivity 退出的時候,CustomRunnable 依然在後臺運行,因此,即使是 MemoryLeakThreadActivity 退出了,但它依然沒法釋放,由於 CustomRunnable 一直在運行且持有對 MemoryLeakThreadActivity 的引用。

  3. 解決方法

    將 CustomRunnable 類聲明爲靜態,此時雖然解除了 MemoryLeakThreadActivity 和 CustomRunnable 之間的關聯關係,但 MemoryLeakThreadActivity 退出的時候,CustomRunnable 卻依然在運行,所以,此時除了將 CustomRunnable 聲明爲靜態以外,還要在 MemoryLeakThreadActivity 的 onDestroy() 方法中,將 CustomRunnable 中止。

    事實上,在此案例中,只用在 MemoryLeakThreadActivity 退出的時候將 CustomRunnable 停掉就行了,但考慮到實際使用場景中每每比這裏的例子要複雜不少,因此,最好的辦法仍是「將 CustomRunnable 類聲明爲靜態」&「在 MemoryLeakThreadActivity 的 onDestroy() 方法中,將 CustomRunnable 中止」。

public class MemoryLeakThreadActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mDoBackgroundTask;
    private static boolean isNeedStop = false;
    private CustomRunnable mCustomRunnable;
    private Thread mThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_thread);
        initView();
    }

    private void initView() {
        mDoBackgroundTask = findViewById(R.id.do_background_task);
        mDoBackgroundTask.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(mThread == null){
            isNeedStop = false;
            mCustomRunnable = new CustomRunnable();
            mThread = new Thread(mCustomRunnable);
            mThread.start();
        }else{
            Toast.makeText(this, "任務已經在運行", Toast.LENGTH_SHORT).show();
        }
    }

    //將 CustomRunnable 聲明爲靜態,此時雖然解除了 MemoryLeakThreadActivity 和 CustomRunnable 之間的關
    //聯關係,但 MemoryLeakThreadActivity 退出的時候,CustomRunnable 卻依然在運行,所以,除了將
    //CustomRunnable 聲明爲靜態以外,還要在 MemoryLeakThreadActivity 的 onDestroy() 方法中,將
    //CustomRunnable 中止
    static class CustomRunnable implements Runnable{

        @Override
        public void run() {
            while (!isNeedStop){
                try {
                    if(Constants.IS_DEBUG){
                        Log.e(Constants.TAG, "CUSTOM RUNNABLE RUNNING");
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isNeedStop = true;
    }

}
複製代碼

相似的狀況還有 AsyncTask:

public class MemoryLeakAsyncTaskActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mDoBackgroundTask;
    private static boolean isNeedStop = false;
    private CustomAsyncTask mCustomAsyncTask;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_thread);
        initView();
    }

    private void initView() {
        mDoBackgroundTask = findViewById(R.id.do_background_task);
        mDoBackgroundTask.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if(mCustomAsyncTask == null){
            isNeedStop = false;
            mCustomAsyncTask = new CustomAsyncTask();
            mCustomAsyncTask.execute();
        }else{
            Toast.makeText(this, "任務已經在運行", Toast.LENGTH_SHORT).show();
        }
    }

    static class CustomAsyncTask extends AsyncTask<Void, Void, Void> {
        @Override
        protected Void doInBackground(Void... voids) {
            while (!isNeedStop){
                try {
                    if(Constants.IS_DEBUG){
                        Log.e(Constants.TAG, "CUSTOM RUNNABLE RUNNING");
                    }
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        isNeedStop = true;
    }

}
複製代碼

5.1.5 資源未關閉

關於這一點,網上有不少博客都是這樣寫的:

對於使用了 BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap 等資源的使用,應該在 Activity 銷燬時及時關閉或者註銷,不然這些資源將不會被回收,形成內存泄漏

因而,我按照上面的說法寫了一個關於 BraodcastReceiver 內存泄漏的例子:

//1. MemoryLeakBroadcastReceiverActivity
public class MemoryLeakBroadcastReceiverActivity extends AppCompatActivity implements View.OnClickListener {

    private Button mSendBroadcast;
    private CustomBroadcastReceiver mBroadcastReceiver;
    private IntentFilter mIntentFilter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_leak_broadcast_receiver);
        initView();
        initData();
    }

    private void initView(){
        mSendBroadcast = findViewById(R.id.send_broadcast);
        mSendBroadcast.setOnClickListener(this);
    }

    private void initData(){
        mBroadcastReceiver = new CustomBroadcastReceiver();
        mIntentFilter = new IntentFilter();
        mIntentFilter.addAction(Constants.CUSTOM_BROADCAST);
        registerReceiver(mBroadcastReceiver, mIntentFilter);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Constants.CUSTOM_BROADCAST);
        sendBroadcast(intent);
    }

    class CustomBroadcastReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context, Constants.CUSTOM_BROADCAST, Toast.LENGTH_SHORT).show();
        }

    }
    
}
複製代碼

反覆打開、關閉 MemoryLeakBroadcastReceiverActivity 併發送廣播,在 Profiler 中看到內存中有兩個 MemoryLeakBroadcastReceiverActivity 實例:

回到 MainActivity,等待一分鐘以後,再次查看 Profiler,發現此時內存中已經沒有 MemoryLeakBroadcastReceiverActivity 實例,也就是說,此時並無內存泄漏的狀況發生:

其餘的關於資源釋放的例子,我沒有嘗試,我只試了這一種,沒有成功的復現內存泄漏。至於爲何那麼多博客都那麼寫,我就不知道了,是個人姿式不對嗎?煩請知道的大佬幫忙解惑。

其實,不管會不會發生內存泄漏都不重要,重要的是要有一個好的習慣——使用了資源,就該在使用完以後及時釋放該資源。

關於內存泄漏,我遇到的問題就這麼多,後面再遇到新的問題再補充該部份內容。

5.2 內存抖動

內存抖動是指在短期內頻繁建立、銷燬大量對象的現象,因爲頻繁地建立、銷燬大量對象,因此致使頻繁 GC。又由於 GC 運行的時候,會將全部進程停掉,因此當內存抖動嚴重的時候,會致使 APP 卡頓。

舉個簡單的例子,在循環中,頻繁地建立字符串:

//1. MemoryShakeActivity
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mContentView;
    private Button mShowContent;
    private int mLength = 1000;
    private String mContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_shake);
        initView();
    }

    private void initView(){
        mContentView = findViewById(R.id.content);
        mShowContent = findViewById(R.id.show_content);
        mShowContent.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        showContent();
    }

    private void showContent(){
        new Thread(){
            @Override
            public void run() {
                Random random = new Random();
                for (int i = 0; i < mLength; i++) {
                    for (int j = 0; j < mLength; j++) {
                        mContent += random.nextInt(mLength);
                    }
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mContentView.setText(mContent);
                    }
                });
            }
        }.start();
    }

}
複製代碼

點擊了 MemoryShakeActivity 界面中的「SHOW CONTENT」按鈕以後,showContent() 方法便開始執行,即開始頻繁建立字符串對象:

在 Profile 中看到,GC 確實一直在執行:

另外,隨着點擊「SHOW CONTENT」按鈕的次數增多,界面也開始卡頓起來。解決辦法也很簡單——減小對象的建立次數:

//1. MemoryShakeActivity 修改以後
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mContentView;
    private Button mShowContent;
    private Random mRandom;
    private int mLength = 100;
    private StringBuilder mStringBuilder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory_shake);
        initView();
        initData();
    }

    private void initView(){
        mContentView = findViewById(R.id.content);
        mShowContent = findViewById(R.id.show_content);
        mShowContent.setOnClickListener(this);
    }

    private void initData(){
        mRandom = new Random();
        if(mStringBuilder == null){
            mStringBuilder = new StringBuilder();
        }
    }

    @Override
    public void onClick(View v) {
        showContent();
    }

    private void showContent(){
        for (int i = 0; i < mLength; i++) {
            for (int j = 0; j < mLength; j++) {
                mStringBuilder.append(mRandom.nextInt(mLength));
            }
        }
        mContentView.setText(mStringBuilder.toString());
        mStringBuilder.delete(0, mStringBuilder.toString().length());
    }

}
複製代碼

更改完字符串的建立方式以後,屢次點擊「SHOW CONTENT」按鈕,Profile 檢測結果以下:

其實,內存抖動除了會形成卡頓以外,還會形成 OOM,主要緣由是有大量小的對象頻繁建立、銷燬會致使內存碎片,從而當須要分配內存時,雖然整體上仍是有剩餘內存可分配,但因爲這些內存不連續,致使沒法分配,系統直接就返回 OOM 了。

因爲內存抖動是由於短期內頻繁建立、銷燬大量對象致使的,因此,給出幾點避免內存抖動的建議:

  1. 儘可能不要在循環內建立對象;
  2. 不要在 View 的 onDraw() 方法內建立對象;
  3. 當須要大量使用 Bitmap 時,嘗試將它們緩存起來以實現複用;

5.3 圖片優化

在 Android 中,關於圖片的優化是一個不得不提的話題,由於它佔用的空間、內存不容忽視。因此,接下來,咱們從如下三個方面討論圖片的優化:

  1. 圖片佔內存空間大小;
  2. Bitmap 高效加載;
  3. 超大圖加載;

5.3.1 圖片佔內存空間大小

5.3.1.1 常見的圖片格式有哪些(以及每種圖片的格式的特色)?應該選擇哪一種圖片格式?

常見的圖片格式有:

  1. PNG;
  2. JPEG;
  3. WEBP;
  4. GIF;

1. PNG

一種無損壓縮的圖片格式,支持完整的透明通道,佔用的內存空間比較大,所以,在使用此類圖片時須要將其壓縮。

2. JPEG

一種有損壓縮的圖片格式,不支持透明通道。

3. WEBP

Google 在 2010 年發佈的,支持有損和無損壓縮,支持完整的透明通道,也支持多幀動畫,是一種理想的圖片格式,因此在「既須要保證圖片質量又須要保證圖片佔用內存空間大小」的時候,它是一個不錯的選擇。

4. GIF

一種支持多幀動畫的圖片格式。

綜上,在開發時,應儘可能使用 WEBP 格式,由於它既能保證圖片質量又能保證圖片佔用內存空間大小。

例如,如今有張 JPEG 格式的圖片,將其從 JPEG 轉化爲 WEBP 以後,圖片所佔的大小變化狀況以下:

3.8 * 1024/668 = 5.8
複製代碼

圖片佔用內存空間的大小足足減小了五倍。

而兩張圖片的實際運行效果卻相差無幾:

你能看出來,兩張圖片實際運行效果有什麼區別嗎?反正,我沒看出來。

5.3.1.2 圖片應該放在哪一個文件夾下?爲何這麼放(內存消耗區別)?

在 Android 項目中,有五個經常使用的放圖片的文件夾,分別是:

  • drawable-ldpi;
  • drawable-mdpi;
  • drawable-hdpi;
  • drawable-xhdpi;
  • drawable-xxhdpi;

當你把同一張照片放在不一樣文件夾下時,解析出來的 Bitmap 的佔用內存空間大小是不一樣的:

//1. MemoryPictureActivity
public class MemoryPictureActivity extends AppCompatActivity implements View.OnClickListener {

    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_picture);
        initView();
    }

    private void initView(){
        mImageView = findViewById(R.id.picture);
        mImageView.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a_background);
        if(Constants.IS_DEBUG){
            Log.e(Constants.TAG, String.valueOf(getBitmapSize(bitmap)));
        }
    }

    public int getBitmapSize(Bitmap bitmap) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            //API 19
            return bitmap.getAllocationByteCount();
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            //API 12
            return bitmap.getByteCount();
        }
        //API 12 以前(在低版本中用一行的字節x高度)
        return bitmap.getRowBytes() * bitmap.getHeight();
    }

}
複製代碼

當把上面的圖片放在 drawable-xxhdpi 文件夾下時,Bitmap 的佔用內存空間大小是 73500000 B(70.1 M),當把上面的圖片放在 drawable-xhdpi 文件夾下時,Bitmap 的佔用內存空間大小是 165375000 B(157.7 M)。

這是由於 Bitmap decodeResource() 方法在解析時會根據當前設備屏幕像素密度 densityDpi 的值進行縮放適配操做,使得解析出來的 Bitmap 與當前設備的分辨率匹配,達到一個最佳的顯示效果:

//1. decodeResource(Resources res, int id)
public static Bitmap decodeResource(Resources res, int id) {
    return decodeResource(res, id, null);
}

//2. decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeResource(Resources res, int id, Options opts) {
    validate(opts);
    Bitmap bm = null;
    InputStream is = null; 
    
    try {
        final TypedValue value = new TypedValue();
        is = res.openRawResource(id, value);

        bm = decodeResourceStream(res, value, is, null, opts);
    } catch (Exception e) {
        /*  do nothing.
            If the exception happened on open, bm will be null.
            If it happened on close, bm is still valid.
        */
    } finally {
        try {
            if (is != null) is.close();
        } catch (IOException e) {
            // Ignore
        }
    }

    if (bm == null && opts != null && opts.inBitmap != null) {
        throw new IllegalArgumentException("Problem decoding into existing bitmap");
    }

    return bm;
}

//3. decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts)
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
            @Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
    validate(opts);
    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }
    
    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }
    
    return decodeStream(is, pad, opts);
}
複製代碼

因此,當只有一張圖片時,直接把它放到 X 最多的 Drawable 文件夾裏面就行了,本例中直接將其放置在「drawable-xxhdpi」文件夾下就行了。

5.3.2 Bitmap 高效加載

Bitmap 高效加載,將從下面三個方面展開描述:

  1. Bitmap 尺寸大小壓縮;
  2. Bitmap 質量壓縮;
  3. Bitmap 複用;
5.3.2.1 Bitmap 尺寸大小壓縮

一般狀況下,能夠認爲一張圖片佔用的內存空間大小爲:

Bitmap 佔用內存空間大小 = 圖片長度 x 圖片寬度 x 單位像素佔用的字節數

單位像素佔用的字節數爲:

可能的位圖配置(Possible Bitmap Configurations) 每一個像素佔用內存(單位:byte)
ALPHA_8 1
ARGB_4444 2
ARGB_8888(默認) 4
RGB_565 2

其中 ARGB_8888 是默認的圖片位圖配置。

由上面的公式可知,「Bitmap 佔用內存空間大小」與 Bitmap 的尺寸大小成正比,所以,當一張大圖在手機上顯示時,爲了節省內存能夠根據目標控件的尺寸將圖片對應的 Bitmap 的尺寸進行縮放,即下面要說的「採樣率縮放」。

所謂採樣率縮放,就是將 Bitmap 的尺寸縮放爲原來 Bitmap 的 1/n,具體步驟以下:

  1. 獲取 Bitmap 原尺寸;
  2. 根據 Bitmap 原尺寸和目標控件的尺寸計算出採樣率(inSampleSize);
  3. 根據計算出的採樣率從新獲取 Bitmap;

示例:

//1. MemoryPictureActivity
public class MemoryPictureActivity extends AppCompatActivity implements View.OnClickListener {

    private ImageView mImageView;
    private Button mBitmapSizeCompress;
    private Button mBitmapQualityCompress;
    private Bitmap mDstBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_picture);
        initView();
    }

    private void initView() {
        mImageView = findViewById(R.id.picture);
        mBitmapSizeCompress = findViewById(R.id.memory_picture_size_compress);
        mBitmapQualityCompress = findViewById(R.id.memory_picture_quality_compress);
        mImageView.setOnClickListener(this);
        mBitmapSizeCompress.setOnClickListener(this);
        mBitmapQualityCompress.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.picture:
                showBitmapSize();
                break;
            case R.id.memory_picture_size_compress:
                compressPictureSizeProxy();
                break;
            case R.id.memory_picture_quality_compress:
                break;
        }
    }

    //1. 獲取 Bitmap 尺寸
    private void showBitmapSize(){
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a_background);
        if (Constants.IS_DEBUG) {
            Log.e(Constants.TAG, String.valueOf(getBitmapSize(bitmap)));
        }
    }

    public int getBitmapSize(Bitmap bitmap) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            //API 19
            return bitmap.getAllocationByteCount();
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
            //API 12
            return bitmap.getByteCount();
        }
        //API 12 以前(在低版本中用一行的字節x高度)
        return bitmap.getRowBytes() * bitmap.getHeight();
    }

    //2. 圖片尺寸壓縮
    private void compressPictureSizeProxy(){
        mImageView.setImageBitmap(compressPictureSize(getResources(), R.drawable.a_background, mImageView.getWidth(), mImageView.getHeight()));
    }

    /**
     * 根據目標 View 的尺寸壓縮圖片返回 bitmap
     * @param resources
     * @param resId
     * @param dstWidth 目標view的寬
     * @param dstHeight 目標view的高
     * @return
     */
    public Bitmap compressPictureSize(final Resources resources, final int resId, final int dstWidth , final int dstHeight){
        //3. 根據目標 View 的尺寸縮放 Bitmap,並進行非空判斷,以免每次都從新建立 Bitmap
        if(mDstBitmap == null){
            mDstBitmap = BitmapFactory.decodeResource(resources, resId);
            if (Constants.IS_DEBUG) {
                Log.e(Constants.TAG, "Before Compress: " + + getBitmapSize(mDstBitmap)/1024/1024f + " M");
            }
            
            BitmapFactory.Options options = new BitmapFactory.Options();
            //3.1 解碼 Bitmap 時,不分配內存,僅獲取 Bitmap 寬、高
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(resources, resId, options);
            int height = options.outHeight;
            int width = options.outWidth;
            int inSampleSize = 1;
            //3.2 根據 Bitmap 原尺寸和目標控件的尺寸計算出採樣率(inSampleSize)
            if (height > dstHeight || width > dstWidth) {
                int heightRatio = Math.round((float) height / (float) dstHeight);
                int widthRatio = Math.round((float) width / (float) dstWidth);
                inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
            }
            options.inSampleSize = inSampleSize;
            options.inJustDecodeBounds = false;
            //3.3 根據計算出的採樣率從新解碼 Bitmap
            mDstBitmap = BitmapFactory.decodeResource(resources,resId,options);
            if (Constants.IS_DEBUG) {
                Log.e(Constants.TAG, "After Compress: " + getBitmapSize(mDstBitmap)/1024/1024f + " M");
            }
        }
        return mDstBitmap;
    }

}

//2. Log 輸出結果:  
2019-09-25 19:08:52.251 30546-30546/com.smart.a17_memory_optimization E/TAG: Before Compress:  157.71387 M
2019-09-25 19:08:52.547 30546-30546/com.smart.a17_memory_optimization E/TAG: After Compress:  6.307617 M
複製代碼

你沒看錯,壓縮前和壓縮後的差距就是這麼大。儘管圖片的格式已經由 JPEG 轉爲 WEBP 格式,但解碼 Bitmap 時仍是很耗內存的,因此,趕忙優化下你 App 裏面用到 Bitmap 的地方吧。

5.3.2.2 Bitmap 質量壓縮

由《5.3.2.1 Bitmap 尺寸大小壓縮》分析可知,默認狀況下的位圖配置是 ARGB_8888,此時單位像素佔用的內存空間是是 4 個字節,因此,能夠在解碼 Bitmap 的時候,利用單位像素佔用字節少的位圖配置,如 RGB_565,單位像素它佔用的內存空間是是 2 個字節。所以,解碼同一張圖片的 Bitmap 時,當位圖配置由 ARGB_8888 切換至 RGB_565 時,內存佔用將減小 1/2。

//1. Bitmap 質量壓縮核心代碼
private void compressPictureQualityProxy(){
    //原生,不作任何處理,解碼 Bitmap 的時候,將消耗不少內存
    mQualityOptimizedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a_background);
    if (Constants.IS_DEBUG) {
        Log.e(Constants.TAG, "Before Compress: " + + getBitmapSize(mQualityOptimizedBitmap)/1024/1024f + " M");
    }
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    mQualityOptimizedBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a_background, options);
     if (Constants.IS_DEBUG) {
            Log.e(Constants.TAG, "After Compress: " + getBitmapSize(mQualityOptimizedBitmap)/1024/1024f + " M");
        }
    mImageView.setImageBitmap(mQualityOptimizedBitmap);
}

//2. Log 輸出結果:  
2019-09-25 19:49:02.941 454-454/com.smart.a17_memory_optimization E/TAG: Before Compress:  157.71387 M
2019-09-25 19:49:05.502 454-454/com.smart.a17_memory_optimization E/TAG: After Compress:  78.856445 M
複製代碼

效果仍是挺明顯的,不是嗎?

雖然內存空間佔用減小了一半,但一張圖片 78.8 M,仍是有點過度,不是嗎?因此,最主要的仍是從源頭上制止這一切,讓美工切圖的時候把圖作小點,而後開發者在解碼 Bitmap 的時候再稍作優化便可。

5.3.2.3 Bitmap 複用

大多數的 App 都會用到圖片,尤爲一些諮詢類的軟件用的更多,若是每一次查看圖片都是從網絡下載,那用戶的流量恐怕早早的就被耗完了。因此,能夠對此進行優化:當用戶第一次到某界面查看圖片時,直接從網絡獲取圖片,獲取到圖片以後,在內存和本地分別存一份。下次,再到該界面的時候,先從內存裏面找,若是找到了,則直接顯示,若是沒找到,再從本地找,若是找到了,則直接顯示,若是本地也沒有找到,再去網絡獲取。

其實這就是不少人說的三級緩存,有一篇博客說的挺不錯的,你們能夠參考下:

Android中圖片的三級緩存

5.3.3 超大圖加載

由前面的分析可知:Android 系統爲每個 App 分配的內存都是有限制的,一旦 App 使用的內存超限以後,將致使 OOM(Out of Memory),即 App 異常退出。所以,若是直接把一張超大圖顯示出來將致使 OOM。因此,超大圖是不能一次性直接加載進來的。那到底該怎麼作呢?

你們應該都用過地圖類軟件,想一下地圖類軟件是怎麼作的(地圖瓦片),這個問題天然有答案了。沒錯,就是一塊一塊加載,好比只加載屏幕中顯示的部分。

其實 Android 早就給提供咱們解決方案了——BitmapRegionDecoder。

BitmapRegionDecoder

BitmapRegionDecoder can be used to decode a rectangle region from an image. BitmapRegionDecoder is particularly useful when an original image is large and you >only need parts of the image.

To create a BitmapRegionDecoder, call newInstance(...). Given a BitmapRegionDecoder, users can call decodeRegion() repeatedly to get a decoded Bitmap of the >specified region.

BitmapRegionDecoder 的用法也很簡單:

  1. 建立 BitmapRegionDecoder 實例;
  2. 顯示指定區域;
//1. 建立 BitmapRegionDecoder 實例
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(InputStream is, boolean isShareable);
或
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(FileDescriptor fd, boolean isShareable);
或
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(String pathName, boolean isShareable);
或
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(byte[] data, int offset, int length, boolean isShareable);

//2. 顯示指定區域  
decodeRegion(Rect rect, BitmapFactory.Options options);
複製代碼

示例,顯示一張世界地圖:

//超大圖加載
private void loadSuperLargePicture()  {
    try {
        InputStream inputStream = getAssets().open("world.jpg");

        //得到圖片的寬、高
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, options);
        int width = options.outWidth;
        int height = options.outHeight;

        //設置顯示圖片的中心區域
        BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
        options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        Bitmap bitmap = null;
        if(showCompletePicture){
            bitmap = bitmapRegionDecoder.decodeRegion(new Rect(0, 0, width , height), options);
        }else{
            bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 200, height / 2 - 200, width / 2 + 200, height / 2 + 200), options);
        }
        mImageView.setImageBitmap(bitmap);
        showCompletePicture = !showCompletePicture;
    } catch (IOException e) {
        e.printStackTrace();
    }
}
複製代碼

最終效果以下:

固然,我這裏只是作了簡短的介紹。想要更加炫酷的效果,能夠將 BitmapRegionDecoder 跟手勢結合,不過因爲篇幅的限制,我就不說那麼多了,畢竟這篇文章已經很長了,後面有機會的話,我會將 Bitmap 高效加載專門拿出來講道說道的。

5.4 其餘優化

  1. 若是你還在用 ListView,那別忘了在 Adapter 中用 ConvertView 和 ViewHolder;
  2. ViewPager 中懶加載;
  3. 若是數據量很少且 key 的類型已經肯定爲 int 類型,那麼使用 SparseArray 代替 HashMap,由於它避免了自動裝箱的過程;若是數據量很少且 key 的類型已經肯定 long 類型,則使用 LongSparseArray 代替 HashMap;若是數據量很少且 key 爲其它類型,則使用 ArrayMap;

6. 總結

本篇文章從爲何要進行內存優化、如何查看 App 內存限制、App 內存是如何分配的、內存檢測工具的使用和常見的內存性能問題五個方面描述了關於內存優化的知識,其中內存檢測工具的使用和常見的內存性能問題是重中之重,由於,你只有知道了如何檢測內存消耗纔可能發現問題,你只有知道哪裏有可能出問題,纔可能更快地找到並解決問題。

今天的分享就到這裏啦,喜歡的小夥伴歡迎轉發、點贊、關注,不喜歡的小夥伴,也順手點個贊吧~~~


參考文檔

  1. Android 內存優化總結&實踐
  2. Android性能優化之常見的內存泄漏
  3. Android 性能優化的方面方面都在這兒
  4. 知識必備】內存泄漏全解析,今後拒絕ANR,讓OOM遠離你的身邊,跟內存泄漏say byebye
  5. Android中圖片的三級緩存
  6. Android 高清加載巨圖方案 拒絕壓縮圖片
相關文章
相關標籤/搜索