Android性能优化:看完这篇文章,至少解决-APP-中-90-%-的内存异常问题

这里我们得知是一个 ilsLoginListener 引用了 LoginView,我们来看下代码最后怎么解决的。

代码中我们找到了 LoginView 这个类,发现是一个单例中的回调引起的内存泄漏,下面怎么解决勒,请看第七小点。

  1. 2种解决单例中的内存泄漏

  2. 将引用置为 null

/**

  • 销毁监听
    */
    public void unRemoveRegisterListener(){
    mMessageController.unBindListener();
    }
    public void unBindListener(){
    if (listener != null){
    listener = null;
    }
    }
  1. 使用弱引用

//将监听器放入弱引用中
WeakReference listenerWeakReference = new WeakReference<>(listener);

//从弱引用中取出回调
listenerWeakReference.get();

  1. 通过第七小点就能完美的解决单例中回调引起的内存泄漏。

Android 中常见的内存泄漏经典案例及解决方法

  1. 单例

示例 :

public class AppManager {

private static AppManager sInstance;
private CallBack mCallBack;
private Context mContext;

private AppManager(Context context) {
this.mContext = context;
}

public static AppManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new AppManager(context);
}
return sInstance;
}

public void addCallBack(CallBack call){
mCallBack = call;
}
}

  1. 通过上面的单列,如果 context 传入的是 Activity , Service 的 this,那么就会导致内存泄漏。

以 Activity 为例,当 Activity 调用 getInstance 传入 this ,那么 sInstance 就会持有 Activity 的引用,当 Activity 需要关闭的时候需要 回收的时候,发现 sInstance 还持有 没有用的 Activity 引用,导致 Activity 无法被 GC 回收,就会造成内存泄漏

  1. addCallBack(CallBack call) 这样写看起来是没有毛病的。但是当这样调用在看一下勒。

//在 Activity 中实现单例的回调
AppManager.getInstance(getAppcationContext()).addCallBack(new CallBack(){
@Override
public void onStart(){

}
});

这里的 new CallBack() 匿名内部类 默认持有外部的引用,造成 CallBack 释放不了,那么怎么解决了,请看下面解决方法

解决方法:

  1. getInstance(Context context) context 都传入 Appcation 级别的 Context,或者实在是需要传入 Activity 的引用就用 WeakReference 这种形式。

  2. 匿名内部类建议大家单独写一个文件或者

public void addCallBack(CallBack call){
WeakReference mCallBack= new WeakReference(call);
}

  1. Handler

示例:

//在 Activity 中实现 Handler
class MyHandler extends Handler{
private Activity m;
public MyHandler(Activity activity){
m=activity;
}

// class…
}

这里的 MyHandler 持有 activity 的引用,当 Activity 销毁的时候,导致 GC 不会回收造成 内存泄漏。

解决方法:

1.使用静态内部类 + 弱引用
2.在 Activity onDestoty() 中处理 removeCallbacksAndMessages()
@Override
protected void onDestroy() {
super.onDestroy();
if(null != handler){
handler.removeCallbacksAndMessages(null);
handler = null;
}
}

  1. 静态变量

示例:

public class MainActivity extends AppCompatActivity {

private static Police sPolice;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (sPolice != null) {
sPolice = new Police(this);
}
}
}

class Police {
public Police(Activity activity) {
}
}

这里 Police 持有 activity 的引用,会造成 activity 得不到释放,导致内存泄漏。

解决方法:

//1. sPolice 在 onDestory()中 sPolice = null;
//2. 在 Police 构造函数中 将强引用 to 弱引用;

  1. 非静态内部类

参考 第二点 Handler 的处理方式

  1. 匿名内部类

示例:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Thread(){
@Override
public void run() {
super.run();
}
};
}
}

很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程ThreadAsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄露。

解决方法:

//静态内部类 + 弱引用
//单独写一个文件 + onDestory = null;

  1. 未取消注册或回调

示例:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
registerReceiver(mReceiver, new IntentFilter());
}

private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// TODO ------
}
};
}

在注册观察则模式的时候,如果不及时取消也会造成内存泄露。比如使用Retrofit + RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

解决方法:

//Activity 中实现 onDestory()反注册广播得到释放
@Override
protected void onDestroy() {
super.onDestroy();
this.unregisterReceiver(mReceiver);
}

  1. 定时任务

示例:

public class MainActivity extends AppCompatActivity {

/*模拟计数/
private int mCount = 1;
private Timer mTimer;
private TimerTask mTimerTask;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
mTimer.schedule(mTimerTask, 1000, 1000);
}

private void init() {
mTimer = new Timer();
mTimerTask = new TimerTask() {
@Override
public void run() {
MainActivity.this.runOnUiThread(new Runnable() {
@Override
public void run() {
addCount();
}
});
}
};
}

private void addCount() {
mCount += 1;
}
}

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity 的引用不能被 GC 回收,因此当我们 Activity 销毁的时候要立即cancelTimerTimerTask,以避免发生内存泄漏。

解决方法:

//当 Activity 关闭的时候,停止一切正在进行中的定时任务,避免造成内存泄漏。
private void stopTimer() {
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
if (mTimerTask != null) {
mTimerTask.cancel();
mTimerTask = null;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
stopTimer();
}

  1. 资源未关闭

示例:

ArrayList,HashMap,IO,File,SqLite,Cursor 等资源用完一定要记得 clear remove 等关闭一系列对资源的操作。

解决方法:

用完即刻销毁

  1. 属性动画

示例:

动画同样是一个耗时任务,比如在 Activity 中启动了属性动画 (ObjectAnimator) ,但是在销毁的时候,没有调用 cancle 方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用 Activity ,这就造成 Activity 无法正常释放。因此同样要在Activity 销毁的时候 cancel 掉属性动画,避免发生内存泄漏。

解决方法:

@Override
protected void onDestroy() {
super.onDestroy();
//当关闭 Activity 的时候记得关闭动画的操作
mAnimator.cancel();
}

  1. Android 源码或者第三方 SDK

示例:

//如果在开发调试中遇见 Android 源码或者 第三方 SDK 持有了我们当前的 Activity 或者其它类,那么现在怎么办了。

解决方法:

//当前是通过 Java 中的反射找到某个类或者成员,来进行手动 = null 的操作。

内存抖动

什么是内存抖动

内存频繁的分配与回收,(分配速度大于回收速度时) 最终产生 OOM 。

也许下面的录屏更能解释什么是内存抖动

可以看出当我点击了一下 Button 内存就频繁的创建并回收(注意看垃圾桶)。

那么我们找出代码中具体那一块出现问题了勒,请看下面一段录屏

mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
imPrettySureSortingIsFree();
}
});

/**
* 排序后打印二维数组,一行行打印
*/
public void imPrettySureSortingIsFree() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}

for (int i = 0; i < lotsOfInts.length; i++) {
String rowAsStr = “”;
//排序
int[] sorted = getSorted(lotsOfInts[i]);
//拼接打印
for (int j = 0; j < lotsOfInts[i].length; j++) {
rowAsStr += sorted[j];
if (j < (lotsOfInts[i].length - 1)) {
rowAsStr += ", ";
}
}
Log.i(“ricky”, "Row " + i + ": " + rowAsStr);
}
}

最后我们之后是 onClick 中的 imPrettySureSortingIsFree() 函数里面的 rowAsStr += sorted[j]; 字符串拼接造成的 内存抖动 ,因为每次拼接一个 String 都会申请一块新的堆内存,那么怎么解决这个频繁开辟内存的问题了。其实在 Java 中有 2 个更好的 API 对 String 的操作很友好,相信应该有人猜到了吧。没错就是将 此处的 String 换成 StringBuffer 或者 StringBuilder,就能很完美的解决字符串拼接造成的内存抖动问题。

修改后

/**
* 打印二维数组,一行行打印
*/
public void imPrettySureSortingIsFree() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for(int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}

// 使用StringBuilder完成输出,我们只需要创建一个字符串即可, 不需要浪费过多的内存
StringBuilder sb = new StringBuilder();
String rowAsStr = “”;
for(int i = 0; i < lotsOfInts.length; i++) {
// 清除上一行
sb.delete(0, rowAsStr.length());
//排序
int[] sorted = getSorted(lotsOfInts[i]);
//拼接打印
for (int j = 0; j < lotsOfInts[i].length; j++) {
sb.append(sorted[j]);
if(j < (lotsOfInts[i].length - 1)){
sb.append(", ");
}
}
rowAsStr = sb.toString();
Log.i(“jason”, "Row " + i + ": " + rowAsStr);
}
}

这里可以看见没有垃圾桶出现,说明内存抖动解决了。

注意: 实际开发中如果在 LogCat 中发现有这些 Log 说明也发生了 内存抖动 (Log 中出现 concurrent copying GC freed …)

回收算法

ps:我觉得这个只是为了应付面试,那么可以参考这里,我也只了解概念这里就不用在多写了,点击看这个帖子吧

也可以参考掘金的这一篇 GC 回收算法

标记清除算法 Mark-Sweep

复制算法 Copying

标记压缩算法 Mark-Compact

分代收集算法

总结 (只要养成这样的习惯,至少可以避免 90 % 以上不会造成内存异常)

  1. 数据类型: 不要使用比需求更占用空间的基本数据类型

  2. 循环尽量用 foreach ,少用 iterator, 自动装箱也尽量少用

  3. 数据结构与算法的解度处理 (数组,链表,栈树,树,图)

  • 数据量千级以内可以使用 Sparse 数组 (Key为整数),ArrayMap (Key 为对象) 虽然性能不如 HashMap ,但节约内存。
  1. 枚举优化

缺点:

  • 每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存
  • 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的 IO 开销,使我们的应用需要更多的空间
  • 特别是分 Dex 多的大型 APP,枚举的初始化很容易导致 ANR

优化后的代码:可以直接限定传入的参数个数

public class SHAPE {
public static final int TYPE_0=0;
public static final int TYPE_1=1;
public static final int TYPE_2=2;
public static final int TYPE_3=3;

@IntDef(flag=true,value={TYPE_0,TYPE_1,TYPE_2,TYPE_3})
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Model{

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
码讲义、实战项目、讲解视频,并且后续会持续更新**

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-1MmiW2Nx-1712777272000)]

总结:

各行各样都会淘汰一些能力差的,不仅仅是IT这个行业,所以,不要被程序猿是吃青春饭等等这类话题所吓倒,也不要觉得,找到一份工作,就享受安逸的生活,你在安逸的同时,别人正在奋力的向前跑,这样与别人的差距也就会越来越遥远,加油,希望,我们每一个人,成为更好的自己。

  • BAT大厂面试题、独家面试工具包,

  • 资料包括 数据结构、Kotlin、计算机网络、Framework源码、数据结构与算法、小程序、NDK、Flutter
    [外链图片转存中…(img-EtioFedl-1712777272000)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-igjYn8CP-1712777272001)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>