内容介绍
- 使用单例模式的技巧
- 谨慎合理选择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的合理性。