Android 系統爲每個 App 分配的內存都是有限制的,一旦 App 使用的內存超限以後,將致使 OOM(Out of Memory),即 App 異常退出。html
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 都申請不到最大內存空間。緩存
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() 堆
}
}
複製代碼
經常使用的內存檢測工具備:性能優化
經常使用的內存泄漏檢測工具備:bash
Android Profiler 是 Android Studio 自帶的用於檢測 CPU、內存、流量、電量使用狀況的工具。網絡
它的特色是能直觀地展現當前 App 的 CPU、內存、流量、電量使用狀況。併發
打開的方法:app
View → Tool Windows → Profiler
複製代碼
或者直接點擊下面這裏:dom
介於篇幅,我就不在此贅述如何使用 Android Profiler 了,想要了解關於 Android Profiler 的使用方法,請參考 Google 開發者文檔。ide
同上,這個工具我也很少說了,你們自行學習相關知識吧。
LeakCanary 出自 Square 公司,它主要用於檢測內存泄漏,使用也十分簡單。
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;
}
}
複製代碼
最終效果以下:
常見的內存性能問題有:
形成內存泄漏的緣由仍是挺多的,常見的有:
//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 發出的內存泄漏通知:
形成問題的緣由:
概述:
ToastUtils 和 MemoryLeakSingletonActivity 生命週期不一致致使的內存泄漏。
詳述:
ToastUtils 是一個單例類,也就是說,一旦 ToastUtils 建立成功以後,ToastUtils 生命週期將和 App 生命週期同樣長,而它所引用的 Activity 的生命週期卻沒有那麼長,當用戶從該 Activity 退出以後,因爲 ToastUtils 依然持有 Activity 的引用,因此,致使 Activity 沒法釋放,形成內存泄漏。
解決方法
將 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();
}
}
複製代碼
//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 發出的內存泄漏通知:
形成問題的緣由:
概述:
CustomThread 和 MemoryLeakStaticInstanceActivity 生命週期不一致致使的內存泄漏。
詳述:
非靜態內部類 CustomThread 持有外部類 MemoryLeakStaticInstanceActivity 的引用,因爲在聲明 CustomThread 時使用了靜態變量,即 CustomThread 實例與 App 生命週期同樣長,而它所引用的 Activity 的生命週期卻沒有那麼長,當用戶從該 Activity 退出以後,因爲 CustomThread 依然持有 Activity 的引用,因此,致使 Activity 沒法釋放。
解決方法
//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;
}
}
複製代碼
//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 發出的內存泄漏通知:
形成問題的緣由:
概述:
MemoryLeakHandlerActivity 結束的時候,MessageQueue 中有待處理的 Message。
詳述:
非靜態內部類 Handler 持有外部類 MemoryLeakHandlerActivity 的引用,因爲在 Handler 的 handleMessage() 方法的內部,Handler 又給本身發送了消息,因此,即使是 MemoryLeakHandlerActivity 退出了,但它(MemoryLeakHandlerActivity)依然沒法釋放,由於 Handler 一直在運行且持有對 MemoryLeakHandlerActivity 的引用。
解決方法
//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);
}
}
複製代碼
//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 發出的內存泄漏通知:
形成問題的緣由:
概述:
MemoryLeakHandlerActivity 結束的時候,CustomRunnable 仍在運行,且 CustomRunnable 中有 MemoryLeakThreadActivity 的引用,故致使 MemoryLeakHandlerActivity 不能釋放。
詳述:
非靜態內部類 CustomRunnable 持有外部類 MemoryLeakThreadActivity 的引用,因爲 MemoryLeakThreadActivity 退出的時候,CustomRunnable 依然在後臺運行,因此,即使是 MemoryLeakThreadActivity 退出了,但它依然沒法釋放,由於 CustomRunnable 一直在運行且持有對 MemoryLeakThreadActivity 的引用。
解決方法
將 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;
}
}
複製代碼
關於這一點,網上有不少博客都是這樣寫的:
對於使用了 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 實例,也就是說,此時並無內存泄漏的狀況發生:
其餘的關於資源釋放的例子,我沒有嘗試,我只試了這一種,沒有成功的復現內存泄漏。至於爲何那麼多博客都那麼寫,我就不知道了,是個人姿式不對嗎?煩請知道的大佬幫忙解惑。
其實,不管會不會發生內存泄漏都不重要,重要的是要有一個好的習慣——使用了資源,就該在使用完以後及時釋放該資源。
關於內存泄漏,我遇到的問題就這麼多,後面再遇到新的問題再補充該部份內容。
內存抖動是指在短期內頻繁建立、銷燬大量對象的現象,因爲頻繁地建立、銷燬大量對象,因此致使頻繁 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 了。
因爲內存抖動是由於短期內頻繁建立、銷燬大量對象致使的,因此,給出幾點避免內存抖動的建議:
在 Android 中,關於圖片的優化是一個不得不提的話題,由於它佔用的空間、內存不容忽視。因此,接下來,咱們從如下三個方面討論圖片的優化:
常見的圖片格式有:
1. PNG
一種無損壓縮的圖片格式,支持完整的透明通道,佔用的內存空間比較大,所以,在使用此類圖片時須要將其壓縮。
2. JPEG
一種有損壓縮的圖片格式,不支持透明通道。
3. WEBP
Google 在 2010 年發佈的,支持有損和無損壓縮,支持完整的透明通道,也支持多幀動畫,是一種理想的圖片格式,因此在「既須要保證圖片質量又須要保證圖片佔用內存空間大小」的時候,它是一個不錯的選擇。
4. GIF
一種支持多幀動畫的圖片格式。
綜上,在開發時,應儘可能使用 WEBP 格式,由於它既能保證圖片質量又能保證圖片佔用內存空間大小。
例如,如今有張 JPEG 格式的圖片,將其從 JPEG 轉化爲 WEBP 以後,圖片所佔的大小變化狀況以下:
3.8 * 1024/668 = 5.8
複製代碼
圖片佔用內存空間的大小足足減小了五倍。
而兩張圖片的實際運行效果卻相差無幾:
你能看出來,兩張圖片實際運行效果有什麼區別嗎?反正,我沒看出來。
在 Android 項目中,有五個經常使用的放圖片的文件夾,分別是:
當你把同一張照片放在不一樣文件夾下時,解析出來的 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」文件夾下就行了。
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. 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.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 的時候再稍作優化便可。
大多數的 App 都會用到圖片,尤爲一些諮詢類的軟件用的更多,若是每一次查看圖片都是從網絡下載,那用戶的流量恐怕早早的就被耗完了。因此,能夠對此進行優化:當用戶第一次到某界面查看圖片時,直接從網絡獲取圖片,獲取到圖片以後,在內存和本地分別存一份。下次,再到該界面的時候,先從內存裏面找,若是找到了,則直接顯示,若是沒找到,再從本地找,若是找到了,則直接顯示,若是本地也沒有找到,再去網絡獲取。
其實這就是不少人說的三級緩存,有一篇博客說的挺不錯的,你們能夠參考下:
由前面的分析可知: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 實例
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 高效加載專門拿出來講道說道的。
本篇文章從爲何要進行內存優化、如何查看 App 內存限制、App 內存是如何分配的、內存檢測工具的使用和常見的內存性能問題五個方面描述了關於內存優化的知識,其中內存檢測工具的使用和常見的內存性能問題是重中之重,由於,你只有知道了如何檢測內存消耗纔可能發現問題,你只有知道哪裏有可能出問題,纔可能更快地找到並解決問題。
今天的分享就到這裏啦,喜歡的小夥伴歡迎轉發、點贊、關注,不喜歡的小夥伴,也順手點個贊吧~~~