您的位置:首页 > 移动开发 > Android开发

Android中如何节省内存占用

2017-01-17 23:09 253 查看

内容介绍

使用单例模式的技巧

谨慎合理选择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 sInsta
d2c9
nce;
}


类在加载的时候创建好单例对象

过于急切,如果放在集中初始化的地方(如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的合理性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: