1. 为什么要进行内存优化?
Android 系统为每一个 App 分配的内存都是有限制的,一旦 App 使用的内存超限之后,将导致 OOM(Out of Memory),即 App 异常退出。
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: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)
常用的内存泄漏检测工具有:
- LeakCanary
4.1 Android Profiler
Android Profiler 是 Android Studio 自带的用于检测 CPU、内存、流量、电量使用情况的工具。
它的特点是能直观地展示当前 App 的 CPU、内存、流量、电量使用情况。
打开的方法:
View → Tool Windows → Profiler
或者直接点击下面这里:
介于篇幅,我就不在此赘述如何使用 Android Profiler 了,想要了解关于 Android Profiler 的使用方法,请参考 Google 开发者文档。
4.2 MAT(Memory Analyzer Tool)
同上,这个工具我也不多说了,大家自行学习相关知识吧。
4.3 LeakCanary
LeakCanary 出自 Square 公司,它主要用于检测内存泄漏,使用也十分简单。
- 在 build.gradle 中添加依赖;
- 在 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. 常见的内存性能问题都有哪些?如何检测?如何解决?
常见的内存性能问题有:
- 内存泄漏;
- 内存抖动;
- 图片优化;
- 其他优化;
5.1 内存泄漏
造成内存泄漏的原因还是挺多的,常见的有:
- 单例(Context 使用不当);
- 创建非静态内部类的静态实例;
- Handler 未及时解除关联的 Message;
- 线程;
- 资源未关闭;
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 发出的内存泄漏通知:
造成问题的原因:
-
概述:
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();
}
}
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 发出的内存泄漏通知:
造成问题的原因:
-
概述:
CustomThread 和 MemoryLeakStaticInstanceActivity 生命周期不一致导致的内存泄漏。
-
详述:
非静态内部类 CustomThread 持有外部类 MemoryLeakStaticInstanceActivity 的引用,由于在声明 CustomThread 时使用了静态变量,即 CustomThread 实例与 App 生命周期一样长,而它所引用的 Activity 的生命周期却没有那么长,当用户从该 Activity 退出之后,由于 CustomThread 依然持有 Activity 的引用,所以,导致 Activity 无法释放。
-
解决方法
- 声明 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 发出的内存泄漏通知:
造成问题的原因:
-
概述:
MemoryLeakHandlerActivity 结束的时候,MessageQueue 中有待处理的 Message。
-
详述:
非静态内部类 Handler 持有外部类 MemoryLeakHandlerActivity 的引用,由于在 Handler 的 handleMessage() 方法的内部,Handler 又给自己发送了消息,所以,即便是 MemoryLeakHandlerActivity 退出了,但它(MemoryLeakHandlerActivity)依然无法释放,因为 Handler 一直在运行且持有对 MemoryLeakHandlerActivity 的引用。
-
解决方法
- 将 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 发出的内存泄漏通知:
造成问题的原因:
-
概述:
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;
}
}
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 了。
由于内存抖动是因为短时间内频繁创建、销毁大量对象导致的,所以,给出几点避免内存抖动的建议:
- 尽量不要在循环内创建对象;
- 不要在 View 的 onDraw() 方法内创建对象;
- 当需要大量使用 Bitmap 时,尝试将它们缓存起来以实现复用;
5.3 图片优化
在 Android 中,关于图片的优化是一个不得不提的话题,因为它占用的空间、内存不容忽视。所以,接下来,我们从以下三个方面讨论图片的优化:
- 图片占内存空间大小;
- Bitmap 高效加载;
- 超大图加载;
5.3.1 图片占内存空间大小
5.3.1.1 常见的图片格式有哪些(以及每种图片的格式的特点)?应该选择哪种图片格式?
常见的图片格式有:
- PNG;
- JPEG;
- WEBP;
- 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 高效加载,将从下面三个方面展开描述:
- Bitmap 尺寸大小压缩;
- Bitmap 质量压缩;
- 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,具体步骤如下:
- 获取 Bitmap 原尺寸;
- 根据 Bitmap 原尺寸和目标控件的尺寸计算出采样率(inSampleSize);
- 根据计算出的采样率重新获取 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 都会用到图片,尤其一些咨询类的软件用的更多,如果每一次查看图片都是从网络下载,那用户的流量恐怕早早的就被耗完了。所以,可以对此进行优化:当用户第一次到某界面查看图片时,直接从网络获取图片,获取到图片之后,在内存和本地分别存一份。下次,再到该界面的时候,先从内存里面找,如果找到了,则直接显示,如果没找到,再从本地找,如果找到了,则直接显示,如果本地也没有找到,再去网络获取。
其实这就是很多人说的三级缓存,有一篇博客说的挺不错的,大家可以参考下:
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 的用法也很简单:
- 创建 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 高效加载专门拿出来说道说道的。
5.4 其他优化
- 如果你还在用 ListView,那别忘了在 Adapter 中用 ConvertView 和 ViewHolder;
- ViewPager 中懒加载;
- 如果数据量不多且 key 的类型已经确定为 int 类型,那么使用 SparseArray 代替 HashMap,因为它避免了自动装箱的过程;如果数据量不多且 key 的类型已经确定 long 类型,则使用 LongSparseArray 代替 HashMap;如果数据量不多且 key 为其它类型,则使用 ArrayMap;
6. 总结
本篇文章从为什么要进行内存优化、如何查看 App 内存限制、App 内存是如何分配的、内存检测工具的使用和常见的内存性能问题五个方面描述了关于内存优化的知识,其中内存检测工具的使用和常见的内存性能问题是重中之重,因为,你只有知道了如何检测内存消耗才可能发现问题,你只有知道哪里有可能出问题,才可能更快地找到并解决问题。
今天的分享就到这里啦,喜欢的小伙伴欢迎转发、点赞、关注,不喜欢的小伙伴,也顺手点个赞吧~~~