内容介绍

  • 使用单例模式的技巧
  • 谨慎合理选择Android的集合
  • 如何更好控制Activity的实例创建
  • 枚举的替代方案
  • Android中那些被隐式创建的对象们
  • 关于减少内存占用,这些细节必须知道

内存介绍

  • JVM运行时数据区:程序计数器,JVM栈,堆内存,方法区,运行时常量池,本地方法栈
  • 程序计数器:用来记录当前正在执行的指令,线程私有。占用空间很小,唯一一个不抛出OOM的区域。
  • JVM栈:存放栈帧,一个栈帧随着一个方法的调用开始而创建,调用完成而结束
  • 堆内存:用来存放对象和数组,多个线程共享。堆内存随着JVM启动而创建。
  • 方法区:存放类的信息,比如类加载器引用,属性、方法代码和构造方法和常量等
  • 运行时常量池:是一个类或者接口的class文件中常量池表的运行时展示形式。
  • 本地方法栈:一个支持native方法调用的JVM实现,需要有这样一个数据区,就是本地方法栈。

基本原则

  • 避免创建不必要的对象
  • 不必要的对象可能是显式创建也能是隐式创建
  • 这些不必要的对象,可以直接避免,也可以另辟蹊径绕过
  • 深入细节和原理是发现并解决问题的有效方法
  • 按需创建对象是重中之重

1.单例模式

定义

  • 单例模式,指的是一个类只有一个实例,并且提供一个全局的访问点。
  • 对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级对象而言,是非常可观的一笔系统开销。
  • 由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。
  • 需要统一管理的时候也可以使用单例模式。比如一个AppConfig
  • 然而单例模式也存在不好的地方,比如影响依赖注入、单元测试等

如何创建单例

1.饿汉式(Eager initialization)

//利用类加载机制,生成实例对象
    private static SingleInstance sInstance = new SingleInstance();

    //1.构造方法私有
    private SingleInstance() {

    }

    //2.全局公开获取实例对象的的方法
    public static SingleInstance getsInstance() {
        return sInstance;
    }
  • 类在加载的时候创建好单例对象
  • 过于急切,如果放在集中初始化的地方(如application 或者 activity.onCreate()方法),可能会降低性能

2.懒汉式(lazy initialization)

private static SingleInstance sInstance;

    private SingleInstance() {

    }

    public static SingleInstance getInstance() {
        if (null == sInstance) {
            sInstance = new SingleInstance();
        }
        return sInstance;
    }
  • 当真正使用单例时才创建
  • 以上写法在如果单例只在单一线程使用,是没有问题的。但是多线程就可能有问题。

3.Synchronized修饰方法

private static SingleInstance sInstance;

    private SingleInstance() {

    }

    public static synchronized SingleInstance getsInstance() {
        if (null == sInstance) {
            sInstance = new SingleInstance();
        }
        return sInstance;
    }

使用synchronized修饰getInstance方法后必然会导致性能下降,而且getInstance是一个被频繁调用的方法。虽然这种方法能解决问题,但是不推荐

4.双重检查

private static volatile SingleInstance sInstance;

    private SingleInstance() {

    }

    public SingleInstance getsInstance(){
        if (null == sInstance) {
            synchronized(SingleInstance.class){
                if (null == sInstance) {
                    sInstance = new SingleInstance();
                }
            }
        }
        return sInstance;
    }

volatile它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。使用volatile修饰sInstance变量之后,可以确保多个线程之间正确处理sInstance变量。

5.利用java的static特性

private SingleInstance() {

    }

    public static SingleInstance getInstance() {
        return SingleInstanceHolder.sInstance;
    }

    private static class SingleInstanceHolder {
        private static SingleInstance sInstance = new SingleInstance();
    }

在java中,类的静态初始化会在类被加载时触发,我们利用这个原理,可以实现利用这一特性,结合内部类

6.使用枚举创建单例

public enum EasySingleton{
        INSTANCE;
}

反编译发现,枚举是在调用的时候进行new生成对象。

2.集合的问题

  • 常用的集合:ArrayList,HashMap,ContentValues等
  • 问题:默认初始容量小,多次扩容(基于数组的容器)
  • 问题:原始类型发生自动装箱

扩容:以ArrayList add方法扩容为例

初始容量小,多次扩容

private static int newCapacity(int currentCapacity){
    int increment = (currentCapacity < (MIN_CAPACITY_INCREMENT / 2)? MIN_CAPACITY_INCREMENT:currentCapacity >> 1);
    return currentCapacity + increment;
}
  • 如果当前容量小于MIN_CAPACITY_INCREMENT的一半,则扩容至currentCapacity + MIN_CAPACITY_INCREMENT
  • 如果当前容量大于MIN_CAPACITY_INCREMENT的一半,则扩容至1.5 x currentCapacity
  • ArrayList中MIN_CAPACITY_INCREMENT的值为12
  • 默认情况下初始容量为0

如何解决频繁扩容
在可以(大概)预知目标数据容量的情况下,设定合理的初始容量
合理选择数据结构,比如某些场景下,我们可以使用基于链表的结果,如可以,这里的ArrayList可以替换成LinkedList

自动发生的装箱

装箱就是java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱
自动装箱指的是装箱操作由compiler自动完成
向集合中添加原始类型的元素,则会发生自动装箱操作,即实际存放如集合的伪装箱后的对象。
从集合中读取这些装箱的元素,可能发生自动拆箱,即自动装箱的逆过程。
一些可以避免自动装箱的集合:
SpareseArray,SparseBooleanArray,SparseIntArray,LongSparseArray等
在时间与空间对比合理时,可以考虑用上述不发生自动装箱的集合。

3.控制Activity的创建

使用正确的launchmode

1.通常我们声明Activity

<activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:theme="@style/AppTheme.NoActionBar">
</activity>

2.启动一个Activity

private void startActivity(){
    startActivity(new Intent(this, MainActivity.class));
}

当我们多次调用startActivity方法,会创建多个MainActivity示例

下面介绍四种Launchmode

  • standard,默认的启动模式,每当有一次Intent请求,就会创建一个新的Activity实例
  • singleTop,如果调用的目标Activity已经位于调用者的Task的栈顶,则不创建新实例,而是使用当前的这个Activity实例,并调用这个实例的onNewIntent方法。
  • singleTask,使用singleTask启动模式的Activity在系统中只会存在一个实例。如果这个实例已经存在,intent就会通过onNewIntent传递到这个Activity(所有位于该Activity上面的Activity实例都将被销毁掉)。否则新的Activity实例被创建。
  • singleInstance这个模式和singleTask差不多,因为他们在系统中都只有一份实例。唯一不同的就是存放singleInstance
    Activity实例的Task只能存放一个该模式的Activity实例。普通app很少用到这个。

3.处理运行时变化方法

  • 当运行变化时保留实例
    调用setRetainInstance(true)方法
  • 手动处理运行时变化
    在manifest配置文件中,设置android:configChanges=”orientation”按照屏幕旋转的放向重写onConfigurationChanged
public void onConfigurationChanged(Configuration newConfig){
    super.onConfigurationChanged(newConfig);
    if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
        setContentView(R.layout.portrait_layout);
    } else if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAP) {
        setContentView(R.layout.landscape_layout);
    }
}

4.枚举的替代方案

枚举出现之前

private static final int COLOR_RED = 0;
private static final int COLOR_YELLOW = 1;
private static final int COLOR = 2;

private void setColor(int color){
    //some codes here
}
  • setColor理论上可以接受除上述三种颜色之外的任意int值

一个简单的枚举

public enum Color{
    RED,YELLOW,BLUE
}

private void setColorEnum(Color color){
    //some code 
}

枚举相当于一个对象,占用的内存比传统的数据类型大,所以不建议使用枚举,尤其是安卓中有了替代方案以后。

要用到注解:后边待续

其他避免创建不必要对象的场景
  • 字符串拼接
  • 减少布局层级
  • 提前检查,减少不必要的异常
  • 不要过多创建线程

字符串拼接

public static void main(String[] args){
    String userName = "Andy";
    String age = "24";
    String job = "Developer";
    String info = userName + age + job;
    System.out.println(info);
}
  • 多个字符串拼接,实现方式为隐式创建一个StringBuilder,然后依次调动append,最后调用toString返回结果字符串。

减少布局层级

  • 布局层级过多,不仅导致inflate过程耗时,还多创建了多余的辅助布局。所以减少辅助布局还是很有必要的。可以尝试其他布局方式或者自定义视图来解决这类的问题。
  • 如果上面采用LinearyLayout,RelativeLayout,或者自定义View效果则截然不同。

提前检查,减少不必要的异常

不要创建过多的线程

private void testThread(){
    new Thread(){
        @Override
        public void run() {
            super.run();
            //do some IO work
        }
    }.start();
}
  • 这种方式会每次创建一个线程,而线程的创建成本很大
  • 建议使用HandlerThread或者ThreadPool处理耗时任务
  • 不建议使用AsyncTask和Executors
HandlerThread

HandlerThread是一个自带Looper的Thread.
结合Handler我们可以实现post,postAtFrontOfQueue,postAtTime和postDelayed以及对应的sendMessage实现
使用HandlerThread处理本地IO读写操作(数据库,文件),因为本地IO操作大多数的耗时属于毫秒级别,对于单线程+异步队列的形式不会产生较大的阻塞。因此在这个HandlerThread中不适合加入网络IO操作。

为什么不建议AsyncTask

以一个四核手机为例,当我们持续调用AsyncTask任务过程中

  • 在AsyncTask线程数量小于CORE_POOL_SIZE(5)时,会启动新的线程处理任务,不重用之前空闲的线程
  • 当数量超过CORE_POOL_SIZE(5),才开始重用之前的线程处理任务
  • 但是由于AsyncTask属于默认线性执行任务,导致并发执行器总是处于某一个线程工作的状态,因而造成了ThreadPool中其他线程的浪费。同时由于AsyncTask中并不存在allowCoreThreadTimeOut(boolean)的调用,所以ThreadPool中的核心线程即使处于空闲状态也不会销毁掉。
为什么不建议使用Executors
//Executors source code 
public static ExecutorService new FixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(nThreads,nThreads,0L,
        TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

//ThreadPoolExecutor source code 
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue){
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit,workQueue, Executors.defaultThreadFactory(),defaultHandler);
}
  • CORE_POOL_SIZE和MAXIMUM_POOL_SIZE都是同样的值,如果把nThreads当成核心线程数,则无法保证最大并发,而如果当做最大并发线程数,则会造成线程的浪费。因而Executors这样的API导致了我们无法在最大并发数和线程节省上做到平衡。
  • 为了达到最大并发数和线程节省的平衡,建议自行创建ThreadPoolExecutor,根据业务和设备信息确定CORE_POOL_SIZE和MAXIMUM_POOL_SIZE的合理性。