Android性能优化——代码优化(一)


注:根据Android官方的建议,编写高效代码的三个基本准则以下:java

  • 不要作冗余的工做
  • 尽可能避免次数过多的内存分配操做
  • 深刻理解全部语言特性和系统平台的API,具体到Android开发,熟练掌握java语言,并对SDK的API熟悉,了如指掌。

1、数据结构的选择

正确的选择合适的数据结构很重要,对java中常见的数据结构例如ArrayList和LinkedList、HashMap和HashSet等,须要作到对它们的联系与区别有教深刻的理解,这样在编写代码中面临选择时才能做出正确的选择,下面咱们以android开发中使用SparseArray代替HashMap为例进行说明。SparseArray是Android平台特有的稀疏数组的实现,它是Integer到Object的一个映射,在特定场合可用于代替HashMap<Integer,<E>>,提升性能。核心实现是二分法查找算法。android

————————————————————————————————————————程序员

SparseArray家族目前有如下四类:算法

————————————————————————————————————————编程

HashMap<Integer, Boolean> booleanHashMap = new HashMap<>();
SparseBooleanArray booleanArray = new SparseBooleanArray();

HashMap<Integer,Integer>  integerHashMap = new HashMap<>();
SparseIntArray intArray = new SparseIntArray();

HashMap<Integer,Long> longHashMap = new HashMap<>();
SparseLongArray sparseLongArray = new SparseLongArray();

HashMap<Integer,String>  stringHashMap = new HashMap<>();
SparseArray<String> sparseArray = new SparseArray<>();复制代码

————————————————————————————————————————设计模式

须要注意的几点以下:数组

  • SparseArray不是线程安全。
  • 因为要进行二分法查找,所以,SparseArray会对插入的数据按照Key值大小顺序插入。
  • SparseArray对删除操做作了优化,它并不会当即删除这个元素,而是经过设置标识位(DELETED)的方式,后面尝试重用。

在Android工程中运行Lint进行静态代码块分析,会有一个名为AndroidLintUseSparseArrays的检查项,若是违规,它会提示:缓存

————————————————————————————————————————安全

HashMap can be replaced with SparseArraybash

————————————————————————————————————————

这样能够很轻松地找到工程中优化的地方。

2、Handler和内部类的正确用法

————————————————————————————————————————

Android代码中涉及线程间通讯的地方常常会使用Handler,典型的代码结构以下:

————————————————————————————————————————

public class HandlerActivity extends Activity {
    
    private final Handler mLeakyHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };
    
}复制代码

————————————————————————————————————————

使用AndroidLint分析这段代码,会违反检查项AndroidLintHanderLeak,获得以下提示:

————————————————————————————————————————

This Handler class should be static or leaks might occur

————————————————————————————————————————

那么产生内存泄漏的缘由多是什么?Handler和Looper以及MessageQueue一块儿工做的,在Android中,一个应用启动后,系统默认会建立一个为主线程服务的Looper对象,该Looper对象用于处理主线程的全部Message对象,它的生命周期贯穿于整个应用的生命周期。在主线程中使用的Handler都会默认绑定到这个Looper对象。在主线程中建立Handler对象时,它会当即关联主线程Looper对象的MessageQueue,这时发送到MessageQueue中的Message对象都会持有这个Handler对象的引用,这样Looper处理消息时才能回调到Handler的handlerMessage方法。所以,若是Message尚未被处理完成,那么Handler对象也就不会被垃圾回收。

在上面的代码中,将Handler的实例声明为HandlerActivity类的内部类。而在Java语言中,非静态内部匿名类会持有外部类的一个隐式的引用,这样就可能会致使外部类没法被垃圾回收。所以,最终因为MessageQueue中的Message尚未处理完成,就会持有Handler对象的引用,而非静态的Handler对象会持有外部类HandlerActivity的引用,这样Activity没法被垃圾回收,从而致使内存泄漏。

一个明显的会引入内存泄漏的例子以下:

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--2:07
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class HandlerActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //延迟10分钟发送消息

        mLeakyHandler.postAtTime(new Runnable() {
            @Override
            public void run() {
                /***/
            }
        }, 1000 * 60 * 10);
        
    }
}复制代码

————————————————————————————————————————

因为消息延迟10分钟发送,所以,当用户进入这个Activity并退出后,在消息发送并处理完成以前,这个Activity是不会被系统回收(系统内存确实不够使用的状况例外)

如何解决呢?两个方案:

  • 在子线程中使用Handler,这是须要开发者本身建立一个Looper对象,这个Looper对象的生命周期同通常的Java对象,所以这种用法没有问题。
  • 将Handler声明为静态的内部类,前面说过,静态内部类不会持有外部类的引用,所以,也不会引用内存泄漏,经典用法的代码以下。

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--2:07
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class HandlerActivity extends Activity {

    /***
     * 声明一个静态的Handler内部类,并持有外部类的弱引用
     */
    private static class InnerHandler extends Handler {

        private final WeakReference<HandlerActivity> mActivity;

        public InnerHandler(HandlerActivity activity) {
            this.mActivity = new WeakReference<HandlerActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            HandlerActivity activity = mActivity.get();
            if (activity != null) {
                //..
            }
        }
    }

    private final InnerHandler mHandler = new InnerHandler(this);

    /**
     * 静态的匿名内部类不会持有外部类的引用
     */
    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() {
            /****/
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //延迟10分钟发送消息

        mHandler.postAtTime(sRunnable, 1000 * 60 * 10);
    }
     

}复制代码

————————————————————————————————————————

3、正确地使用Context

Context应该是每一个入门Android开发的程序员第一个接触到的概念,它表明当前的上下文环境,能够用来实现不少功能的调用,语句以下:

————————————————————————————————————————

//获取资源管理器对象,进而能够访问到例如string,color等资源
Resources resources = context.getResources();


//启动指定的Activity
context.startActivity(new Intent(this, MainActivity.class));

//获取各类系统服务
TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

//获取系统文件目录
File internalDir = context.getCacheDir();
File externalDir = context.getExternalCacheDir();

//更多。。。复制代码

可见,正确理解Context的概念是很重要的,虽然应用开发中随处可见Context的使用,但并非全部的Context实例都具有相同的功能,在使用上须要区别对待,不然极可能会引入问题。咱们首先来总结下Context的种类。

(1)、Context的种类

根据Context依托的组件以及用途不一样,咱们能够将Context分为以下几种。

  • Application:Android应用中的默认单例类,在Activity或者Service中经过getApplication()能够获取到这个实例,经过context.getApplicationContext() 能够获取到应用全局惟一的Context实例。
  • Activity/Service:这两个类都是ContextWrapper的子类,在这两个类中能够经过getBaseContext()获取到它们的Context实例,不一样的Activity或者Service实例,它们的Context都是独立的,不会复用。
  • BroadcastReceiver:和Activity以及Service不用,BroadcastReceiver自己并非Context的子类,而是在回调函数onReceive()中由Android框架传入一个Context的实例。系统传入的这个Context实例是通过功能裁剪的,它并不能调用registerReceiver()以及bindService()这个两个函数。
  • ContextProvider:一样的,ContentProvider也不是Context的子类,但在建立时系统会传入一个Context实例,这样在ContentProvider中能够经过调用getContext()函数获取。若是ContentProvider和调用者处于相同的应用进程中,那么getContext()将返回应用全局惟一的Context的实例。若是是其余进程调用的ContentProvider,那么ContentProvider将持有自身所在进程的Context实例。

(2)、错误使用Context致使的内存泄漏

错误地使用Context可能会致使内存泄漏,典型的例子是在实现单例模式时使用Context,以下代码是可能会致使内存泄漏的单例实现。

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--23:57
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class SingleInstance {

    private Context mContext;
    private static SingleInstance sInstance;

    private SingleInstance(Context context) {
        mContext = context;
    }

    public static SingleInstance getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new SingleInstance(context);
        }
        return sInstance;
    }
    
}复制代码

————————————————————————————————————————

若是使用者调用getInstance时传入的Context是一个Activity或者Service的实例,那么在应用退出以前,因为单例一直存在,会致使对应的Activity或者Service被单例引用,从而不会被垃圾回收,Activity或者Service中关联的其余View或者数据结构对象也不会被释放,从而致使内存泄漏。正确的作法是使用Application Context,由于它是应用惟一的,并且声明周期是跟着应用一致的,正确的单例实现以下:

————————————————————————————————————————

/**
 * ================================================================
 * User:xijiufu
 * Email:xjfsml@163.com
 * Version:1.0
 * Time:2017/4/20--23:57
 * Function:
 * ModifyHistory:
 * ================================================================
 */
public class SingleInstance {

    private Context mContext;
    private static SingleInstance sInstance;

    private SingleInstance(Context context) {
        mContext = context;
    }

    public static SingleInstance getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new SingleInstance(context.getApplicationContext());//这一句关键
        }
        return sInstance;
    }

}复制代码

(3)、不一样Context的对比

不一样组件中的Context能提供的功能不尽相同,总结起来,以下表:

功能 Application Activity Service BroadcastReceiver ContentProvider
显示Dialog NO YES NO NO NO
启动Activity NO[1] YES NO[1] NO[1] NO[1]
实现LayoutInflation NO[2] YES NO[2] NO[2] NO[2]
启动Service YES YES YES YES YES
绑定Service YES YES YES YES NO
发送Broadcast YES YES YES YES YES
注册Broadcast YES YES YES YES NO[3]
加载资源Resource YES YES YES YES YES

其中NO[1]标记表示对应的组件并非真的不能够启动Activity,而是建议不要这么作,由于这些组件会在新的Task中建立Activity,而不是在原来的Task中。

NO[2]标记也是表示不建议这么作,由于在非Activity中进行Layout Inflation,会使用系统默认的主题,而不是应用中设置的主题。

NO[3]标记表示在Android4.2及以上的系统上,若是注册的BroadcastReceiver是null时是能够的,用来获取sticky广播的当前值。

4、掌握java的四种引用方式

掌握java的四种引用类型对于写出内存使用良好的应用是很关键的。

  • 强引用:Java里面最普遍使用的一种,也是对象默认的引用类型。若是一个对象具备强引用,那么垃圾回收期是不会对它进行回收操做的,当内存空间不足时,Java虚拟机将会抛出OutOfMemoryError错误,这时应用将会终止运行。一句话总结,只要引用存在,垃圾回收器永远不会回收。Object obj = new Object(); 能够直接经过obj取得对应的对象,只有obj这个引用被释放以后,对象才会被释放掉。

  • 软引用:一个对象若是只有软引用,那么当内存空间不足时,垃圾回收器不会对它进行回收操做,只有当内存空间不足时,这个对象才会被回收。软引用能够用来实现内存敏感的高速缓存,若是配合引用队列(ReferenceQueue)使用,当软引用指向的对象被垃圾回收器回收后,Java虚拟机将会把这个软引用加入到与之关联的引用队列中。一句话总结,非必须引用,内存溢出以前进行回收,
    Object object = new Object();
    SoftReference<Object> sf = new SoftReference<Object>(object);复制代码
  • 软引用:弱引用是比软引用更弱的一种引用类型,只有弱引用指向的对象的生命周期更短,当垃圾回收器扫描到只具备弱引用的对象时,不论当前内存空间是否不足,都会对弱引用对象进行回收。弱引用也能够和一个引用队列配合使用,当弱引用指向的对象被回收后,Java虚拟机会将这个弱引用加入到与之关联的引用队列中。一句话总结,
  • Object object = new Object();
    WeakReference<Object> reference = new WeakReference<Object>(object);复制代码
  • 虚引用:和软引用和弱引用不一样,虚引用并不会对所指向的对象生命周期产生任何影响,也就是对象仍是会按照它原来的方式被垃圾回收器回收,虚引用本质上只是一个标记做用,主要用来跟踪对象被垃圾回收的活动,虚引用必须和引用队列配合使用,当对象被垃圾回收时,若是存在虚引用,那么Java虚拟机会将这个虚引用加入与之关联的引用队列中。

5、其余代码微优化

(1)、避免建立非必要的对象

————————————————————————————————————————

对象的建立须要内存分配,对象的销毁须要垃圾回收,这些都会必定程度上影响到应用的性能。所以通常来水,最好是重用对象而不是在每次须要的时候去建立一个功能相同的新对象,特别是注意不要在循环中重复建立相同的对象。

(2)、对常量使用static final 修饰

————————————————————————————————————————

对于基本数据类型和String类型的常量,建议使用static final 修饰,由于final类型的常量会在会进入静态dex文件的域初始化部分,这是对基本数据类型和String类型常量的调用不会涉及类的初始化,而是直接调用字面量。

(3)、避免内部的Getters/Setters

————————————————————————————————————————

在面向对象编程中,Getters/Setters的做用主要是对外屏蔽具体的变量定义,从而达到更好的封装性。但若是是在类内部还使用Getters/Setters函数访问变量的话,会下降访问的速度。根据Android官方文档,在没有JIT(Just In Time)编译器时,直接访问变量的速度是调用Getter方法的3倍;在JIT编译时,直接访问变量的速度是调用Getters方法的7倍。固然,若是你的应用中使用了ProGuard(混淆代码)的话,那么ProGuard会对Getters/Setters进行内联操做,从而达到直接访问的效果。

(4)、代码的重构

代码的重构是一项长期的锲而不舍的工做,须要依靠团队中每个成员来维护代码库的高质量,要会去享受高质量代码带来的快感,如何有效的进行代码重构,除了须要对你所在项目有较深刻的理解以外,你还须要必定的方法指导。重构代码可使用不一样的设计模式来达到高质量的代码,这儿能够关注个人设计模式系列博客:Android设计模式之——单例模式(一) Android设计模式之——Builder模式(二)

相关文章
相关标签/搜索