Android性能优化


项目中的单例

在分析性能优化之前偶然的看到项目中的有很多单例模式,单例模式几乎是项目中被应用最多的设计模式,不同单例模式对性能开销也是不一样的。

  1. 饿汉式
public class HungSingle {
    private static final HungSingle hungSingle = new HungSingle();

    //构造函数私有
    private HungSingle() {

    }

    //公有的静态函数,对外暴露获取单例对象的接口
    public static HungSingle getHungSingle() {
        return hungSingle;
    }

}

HungSingle类不能通过new的形式构造对象,只能通过HungSingle.getHungSingle()方法来获取,而这个HungSingle对象是静态对象,并且在声明的时候就已经初始化,这就保证了HungSingle对象的唯一性。

  1. 懒汉式
public class Singleton {
    private static Singleton instance;

    private Singleton() {
        
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

上述方法中添加synchronized字段保证在多线程情况下单例对象的唯一性,但是会有一个问题,每次调用getInstance()方法都会进行同步,这样会消耗不必要的资源,这也是懒汉式存在的最大问题,这种模式一般不建议使用

  1. Double Check Lock (DCL)单例
public class Singleton {
       private static Singleton sInstance = null;

       private Singleton() {
       
    }

       public static Singleton getInstance() {
        if (sInstance == null) {
            synchronized (Singleton.class) {
                 if (sInstance == null) {
                     sInstance = new Singleton();
                   }
            }
         }
        return sInstance;
       }
   }

可以看到getInstance()方法中对sInstance进行了两次判空,第一层判断主要为了避免不必要的同步,第二层的判断则是为了在null的情况下创建实例。

假设有个线程A执行到了sInstance = new Singleton()语句,它大致做了如下3件事:

  1. Singleton的实例分配内存;
  2. 调用Singleton的私有构造函数,初始化成员字段;
  3. sInstance对象指向分配的内存空间, 此时sInstance对象就不为空了。

JDK 1.5之前上面的第二和第三顺序是无法保证的,如果3执行完、2未执行,这时候被切换到B线程,由于sInstance在线程A内执行过第3步了,sInstance已经是非空了,所以线程B直接取走了sInstance,再使用就会出错,这就是DLC失效问题。 在JDK1.5之后官方加入了volaatile关键字,因此在1.5之后的版本只需要将sInstance的定义改为private volatile static Singleton sInstance = null即可。


  1. 静态内部类单例模式 (最推荐的模式)
public class Singleton{
       private Singleton() {

       }

    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }

    /**
     * 静态内部类
     */
    private static class SingletonHolder {
         private static final Singleton sInstance = new Singleton();
     }
   }


只有在第一次调用SingletongetInstance方法时才会导致sInstance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够保证线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。


项目中的单例

   // 获取单例
    public static Common getInstance() {
        synchronized (Common.class) {
            if (instance == null) {
                instance = new Common();
                activityList = new ArrayList<>();
            }
        }

        return instance;
    }
 public static XunfeiSpeekUtils getInstance() {
        synchronized (XunfeiSpeekUtils.class) {
            if (instance == null) {
                instance = new XunfeiSpeekUtils();
            }
        }

        return instance;
    }

每次调用getInstance()都会进行同步操作,这样是非常不友好的,造成很大的开销。 即使加双重判断锁也会出现DLC失效的问题。

内存泄露

首先要搞清楚内存泄露内存溢出是两个概念,比如一车最多能坐5个人,你却非要塞下10个,车就挤爆了,这就是内存溢出。 而车上的五个人本来应该在车到站后都下车的,结果只下车了3个人,还有两个人一直赖在座位上不肯下来,这就是内存泄露 ,泄露的多了就会导致OOM产生。

非静态内部类

在java中,内部类会隐式的持有外部类的引用,项目中用的比较多的是Handler作为非静态内部类的使用,如果Handlerfragment或者Activity结束的时候扔有未执行完的任务,比如一个定时任务等,那么就会导致内存泄露,项目中非静态内部类的Handler比比皆是:

 private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            Bitmap thumbnailBitmap = (Bitmap) msg.obj;
            mJCVideoPlayerStandard.thumbImageView.setImageBitmap(thumbnailBitmap);
        }
    };

    Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            int what = msg.what;
            switch (what) {
                case READ_ADDRESS_IN_JSON:
                    String json = (String) msg.obj;
                    //得到地址的实体类对象
                    try {
                        JSONObject object = new JSONObject(json);
                        JSONArray array = object.getJSONArray("data");
                        ...

  private Handler handler = new Handler() {
        public void handleMessage(android.os.Message msg) {
            switch (msg.what) {
                //0101绑定指令
                case 11:
                    setWriteCharacteristicNotification("0101", writeBtCharacteristic);
                    cancelScan();
                    Common.getInstance().setBluetoothState(true, bluetoothGatt.getDevice().getName());
                    runOnMainThread(new Runnable() {
                        @Override
                        public void run() {
                            EventBus.getDefault().post(new ConnectSuccessEvent("ConnectSuccess", bluetoothGatt.getDevice().getName()));
                        }
                    });

                    handler.removeMessages(11);//移除消息
                    break;

   Handler myHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1000:
                    NoHttpUtils.httpGet(AppConstant.URL_HEARTBEAT, new HashMap(), mOnResponseListener, REQUEST_HEARTBEAT_CODE);
                    break;
                default:
                    break;
            }
        }
    };
    ...

上面我们分析非静态内部类的Handler可能会导致内存泄露,检测工具就给了我们下面这张图,追踪下去发现是StateLayout产生的,并且会发生在每个Fragment中,清楚源头之后我们点击进入StateLayout类中

public class StateLayout extends FrameLayout {
    private Handler handler = new Handler();
    ...
...
  final Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    switch (switchPosition) {
                        case 0:
                            progressTextView.setText(mContext.getString(R.string.state_loading1));
                            break;
                        case 1:
                            progressTextView.setText(mContext.getString(R.string.state_loading2));
                            break;
                        case 2:
                            progressTextView.setText(mContext.getString(R.string.state_loading3));
                            break;
                        case 3:
                            progressTextView.setText(mContext.getString(R.string.state_loading4));
                            switchPosition = -1;
                            break;
                        default:
                            break;
                    }
                    switchPosition++;
                    handler.postDelayed(this, 500);
                }
            };
            handler.post(runnable);
            ...

果然可以在里面看到一个非静态内部类Handler, 我们可以看到当前类是继承自FrameLayout的,在Android控件中绝大部分都是持有布局对应的fragment或者Activity的引用的,而当前Handler是非静态内部类隐性的持有了外部类的引用,间接的就导致在布局中使用了这个StateLayoutfragmentHandler所持有,即使执行了onDestory,其引用一直无法被回收掉。

解决方案: 这里我将Handler删除,用一个属性动画代替,就不会再出现Handler的内存泄露问题了,不过这里却报了属性动画的内存泄露,查阅相关资料发现,属性动画如果不及时释放也是会导致内存泄露的,最终在显示隐藏回调方法里判断当前StateLayout不再显示的时候将动画释放。

@Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        if (valueAnimator == null) {
            return;
        }
        if (visibility == GONE || visibility == INVISIBLE) {
            valueAnimator.cancel();
            valueAnimator = null;
        }
    }


Context 导致内存泄漏

public class XunfeiSpeekUtils {
    private static XunfeiSpeekUtils instance;

    public static XunfeiSpeekUtils getInstance() {
        synchronized (XunfeiSpeekUtils.class) {
            if (instance == null) {
                instance = new XunfeiSpeekUtils();
            }
        }

        return instance;
    }

    SpeechRecognizer mIat;
    RecognizerDialog mIatDialog;
    HashMap<String, String> mIatResults = new LinkedHashMap<String, String>();
    Context mContext = null;
   ...

 XunfeiSpeekUtils.getInstance().init(mActivity).speak(voice);

上面这行代码是必然会导致内存泄露的,而且露的还不少,我们可以看下面两张图:

红框部分我们点击进去可以看到正是这个XunfeiSpeekUtils导致了2.7MB的内存泄露:

案例分析:XunfeiSpeekUtils是个单例类,生命周期是很长的,持有的contextmActivity,这就导致当前activity在销毁的时候其引用扔被这个单例所持有,这就造成的内存泄露,解决方法是传入getApplicationContext,因为Application的生命周期是贯穿整个程序的,所以XunfeiSpeekUtils类持有它的引用,也不会造成内存泄露问题。


集成LeakCanary内存泄露检测工具到项目

在只监控Activity的时候,打开咱们项目app的时候LeadCanary立刻就发送了通知过来,点击通知栏我们可以看到具体的泄露地方

如何集成这里就不说了,百度都有,而且非常简单。主要来看下怎么理解这个工具的错误信息

1.以LoginActivity为例,由上至下,ProgressDialogUtils中的静态imageView引用的mContext导致了LoginActivity内存泄露。接着我们打开这个导致内存泄露的ProgressDialogUtils一探究竟。

public class ProgressDialogUtils extends AlertDialog {


    private static ImageView imageView;
    private static TextView  textView;

   ...

可以看到这里所有的View控件都是静态的,而我们的ImageView是持有Activity引用的,static变量在内存中是单独存在于内存块中的,这种情况下,Activity是没法被彻底销毁的,因为在内存中一直有一个引用,导致Activity也无法被回收,自然就会内存泄漏了。 建议,在Android中不要使用static修饰控件。

2.接着来看另一个导致内存泄露的MainActivity

public class Player implements OnCompletionListener {
    private MediaPlayer mediaPlayer;
    private PlayerHandler mPlayerhandler;
    Context mContext = null;

    public Player(Context context) {
        mContext = context;
        mPlayerhandler = new PlayerHandler();

这个就比较简单了,Player控件中真正执行操作的其实还是MediaPlayer, MediaPlayer的源码中是持有Activity的引用的,因此在使用完之后需要及时的释放,咱们代码中并未释放过,这里其实还是有个坑的,一般来说释放代码应该像这样的

 /**
     * 释放播放器资源
     */
    private void ReleasePlayer() {
        if (player != null) {
            player.stop();
            player.release();
            player = null;
        }

但是这样写之后,发现还是存在内存泄露,大致原因就是在源码中release方法并未到某个引用做释放动作,而reset方法可以。改成这样:

 /**
     * 释放播放器资源
     */
    private void ReleasePlayer() {
        if (player != null) {
            player.reset();
            player.release();
            player = null;
        }

使用Lint分析

Android Studio内置了Lint,只要点一下就可以使用,Lint 的使用路径:工具栏 -> Analyze -> Inspect Code…

68747470733a2f2f692e696d6775722e636f6d2f7247534e6a45672e706e67.png

自定义View的时候 onDraw onMeasure onLayout等方法会被多次调用 ,如果在这几个方法里去实例化对象,对性能消耗是很大的,比如下面几个:

项目中经常使用Weight属性来做适配,但是如果父布局跟子布局同时都使用Weight是对性能很不好的,解决方法也很简单,去除子布局中非必须的weight。


静态变量造成的内存泄露

结语:Android的性能优化是多方面的,比如启动速度优化、UI流畅度优化、apk瘦身、电量优化、内存优化等,这里只是对内存方面做一个简短的介绍。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容