在编程开发中,内存的占用是我们经常要面对的现实,通常的内存调优的方向就是尽量减少内存的占用。这其中避免创建不必要的对象是一项重要的方面。

Android设备不像PC那样有着足够大的内存,而且单个App占用的内存实际上是比较小的。所以避免创建不必要的对象对于Android开发尤为重要。

本文会介绍一些常见的避免创建对象的场景和方法,其中有些属于微优化,有的属于编码技巧,当然也有确实能够起到显著效果的方法。

使用单例

单例是我们常用的设计模式,使用这种模式,我们可以只提供一个对象供全局调用。因此单例是避免创建不必要的对象的一种方式。

单例模式上手容易,但是需要注意很多问题,最重要的就是多线程并发的情况下保证单例的唯一性。当然方式很多,比如饿汉式,懒汉式double-check等。这里介绍一个很极客的书写单例的方式。

public class SingleInstance {
private SingleInstance() {
} public static SingleInstance getInstance() {
return SingleInstanceHolder.sInstance;
} private static class SingleInstanceHolder {
private static SingleInstance sInstance = new SingleInstance();
}
}

在Java中,类的静态初始化会在类被加载时触发,我们利用这个原理,可以实现利用这一特性,结合内部类,可以实现上面的代码,进行懒汉式创建实例。

避免进行隐式装箱

自动装箱是Java 5 引入的一个特性,即自动将原始类型的数据转换成对应的引用类型,比如将int转为Integer等。

这种特性,极大的减少了编码时的琐碎工作,但是稍有不注意就可能创建了不必要的对象了。比如下面的代码

Integer sum = 0;
for(int i=1000; i<5000; i++){
sum+=i;
}

上面的代码sum+=i可以看成sum = sum + i,但是+这个操作符不适用于Integer对象,首先sum进行自动拆箱操作,进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下

int result = sum.intValue() + i;
Integer sum = new Integer(result);

由于我们这里声明的sum为Integer类型,在上面的循环中会创建将近4000个无用的Integer对象,在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量。因此在我们编程时,需要注意到这一点,正确地声明变量类型,避免因为自动装箱引起的性能问题。

另外,当将原始数据类型的值加入集合中时,也会发生自动装箱,所以这个过程中也是有对象创建的。如有需要避免这种情况,可以选择SparseArray,SparseBooleanArray,SparseLongArray等容器。

谨慎选用容器

Java和Android提供了很多编辑的容器集合来组织对象。比如ArrayList,ContentValues,HashMap等。

然而,这样容器虽然使用起来方便,但也存在一些问题,就是他们会自动扩容,这其中不是创建新的对象,而是创建一个更大的容器对象。这就意味这将占用更大的内存空间。

以HashMap为例,当我们put key和value时,会检测是否需要扩容,如需要则双倍扩容

@Override public V put(K key, V value) {
if (key == null) {
return putValueForNullKey(value);
}
//some code here // No entry for (non-null) key is present; create one
modCount++;
if (size++ > threshold) {
tab = doubleCapacity();
index = hash & (tab.length - 1);
}
addNewEntry(key, value, hash, index);
return null;
}

关于扩容的问题,通常有如下几种方法

  • 预估一个较大的容量值,避免多次扩容
  • 寻找替代的数据结构,确保做到时间和空间的平衡

用好LaunchMode

提到LaunchMode必然和Activity有关系。正常情况下我们在manifest中声明Activity,如果不设置LaunchMode就使用默认的standard模式。

一旦设置成standard,每当有一次Intent请求,就会创建一个新的Activity实例。举个例子,如果有10个撰写邮件的Intent,那么就会创建10个ComposeMailActivity的实例来处理这些Intent。结果很明显,这种模式会创建某个Activity的多个实例。

如果对于一个搜索功能的Activity,实际上保持一个Activity示例就可以了,使用standard模式会造成Activity实例的过多创建,因而不好。

确保符合常理的情况下,合理的使用LaunchMode,减少Activity的创建。

Activity处理onConfigurationChanged

这又是一个关于Activity对象创建相关的,因为Activity创建的成本相对其他对象要高很多。

默认情况下,当我们进行屏幕旋转时,原Activity会销毁,一个新的Activity被创建,之所以这样做是为了处理布局适应。当然这是系统默认的做法,在我们开发可控的情况下,我们可以避免重新创建Activity。

以屏幕切换为例,在Activity声明时,加上

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

然后重写Activity的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_LANDSCAPE) {
setContentView(R.layout.landscape_layout);
}
}

注意字符串拼接

字符串这个或许是最不起眼的一项了。这里主要讲的是字符串的拼接

Log.i(LOGTAG, "onCreate bundle=" + savedInstanceState);

这应该是我们最常见的打log的方式了,然而字符串的拼接内部实际是生成StringBuilder对象,然后挨个进行append,直至最后调用toString方法的过程。

下面是一段代码循环的代码,这明显是很不好的,因为这其中创建了很多的StringBuilder对象。

public void  implicitUseStringBuilder(String[] values) {
String result = "";
for (int i = 0 ; i < values.length; i ++) {
result += values[i];
}
System.out.println(result);
}

降低字符串拼接的方法有

  • 使用String.format替换
  • 如果是循环拼接,建议显式在循环外部创建StringBuilder使用

减少布局层级

布局层级过多,不仅导致inflate过程耗时,还多创建了多余的辅助布局。所以减少辅助布局还是很有必要的。可以尝试其他布局方式或者自定义视图来解决这类的问题。

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

异常对于程序来说,在平常不过了,然后其实异常的代码很高的,因为它需要收集现场数据stacktrace。但是还是有一些避免异常抛出的措施的,那就是做一些提前检查。

比如,我们想要打印一个文件的每一行字符串,没做检查的代码如下,是存在FileNotFoundException抛出可能的。

private void printFileByLine(String filePath) {
try {
FileInputStream inputStream = new FileInputStream("textfile.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String strLine;
//Read File Line By Line
while ((strLine = br.readLine()) != null) {
// Print the content on the console
System.out.println (strLine);
}
br.close();
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

如果我们进行文件是否存在的检查,抛出FileNotFoundException的概率会减少很多,

private void printFileByLine(String filePath) {
if (!new File(filePath).exists()) {
return;
}
try {
FileInputStream inputStream = new FileInputStream("textfile.txt");
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String strLine;
//Read File Line By Line
while ((strLine = br.readLine()) != null) {
// Print the content on the console
System.out.println (strLine);
}
br.close();
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

上述的检查是一个不错的编码技巧,建议采纳。

不要过多创建线程

在android中,我们应该尽量避免在主线程中执行耗时的操作,因而需要使用其他线程。

private void testThread() {
new Thread() {
@Override
public void run() {
super.run();
//do some io work
}
}.start();
}

虽然这些能工作,但是创建线程的代价远比普通对象要高的多,建议使用HandlerThread或者ThreadPool做替换。

使用注解替代枚举

枚举是我们经常使用的一种用作值限定的手段,使用枚举比单纯的常量约定要靠谱。然后枚举的实质还是创建对象。好在Android提供了相关的注解,使得值限定在编译时进行,进而减少了运行时的压力。相关的注解为IntDef和StringDef。

如下以IntDef为例,介绍如何使用

在一个文件中如下声明

public class AppConstants {
public static final int STATE_OPEN = 0;
public static final int STATE_CLOSE = 1;
public static final int STATE_BROKEN = 2; @IntDef({STATE_OPEN, STATE_CLOSE, STATE_BROKEN})
public @interface DoorState {}
}

然后设置书写这样的方法

private void setDoorState(@AppConstants.DoorState int state) {
//some code
}

当调用方法时只能使用STATE_OPENSTATE_CLOSESTATE_BROKEN。使用其他值会导致编译提醒和警告。

选用对象池

在Android中有很多池的概念,如线程池,连接池。包括我们很长用的Handler.Message就是使用了池的技术。

比如,我们想要使用Handler发送消息,可以使用Message msg = new Message(),也可以使用Message msg = handler.obtainMessage()。使用池并不会每一次都创建新的对象,而是优先从池中取对象。

使用对象池需要需要注意几点

  • 将对象放回池中,注意初始化对象的数据,防止存在脏数据
  • 合理控制池的增长,避免过大,导致很多对象处于闲置状态

谨慎初始化Application

Android应用可以支持开启多个进程。 通常的做法是这样

<service android:name=".NetworkService"
android:process=":network"
/>

通常我们在Application的onCreate方法中会做很多初始化操作,但是每个进程启动都需要执行到这个onCreate方法,为了避免不必要的初始化,建议按照进程(通过判断当前进程名)对应初始化.

public class MyApplication extends Application {
private static final String LOGTAG = "MyApplication"; @Override
public void onCreate() {
String currentProcessName = getCurrentProcessName();
Log.i(LOGTAG, "onCreate currentProcessName=" + currentProcessName);
super.onCreate();
if (getPackageName().equals(currentProcessName)) {
//init for default process
} else if (currentProcessName.endsWith(":network")) {
//init for netowrk process
}
} private String getCurrentProcessName() {
String currentProcessName = "";
int pid = android.os.Process.myPid();
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
currentProcessName = processInfo.processName;
break;
}
}
return currentProcessName;
}
}

上面的一些知识就是关于Android中如何避免创建多余对象的总结.欢迎提出意见和观点,共同进步.

【转载】如何在Android中避免创建不必要的对象的更多相关文章

  1. Android DevArt5:如何在Android中创建多线程?

    本篇内容: 如何在Android中创建多进程?查看进程的三种方式有哪些? 多进程模式的运行机制?- 演示了多进程出现问题中的两种情况: 静态成员失效 Application多次创建 IPC基础概念介绍 ...

  2. 如何在Android中使用OpenCV

    如何在Android中使用OpenCV 2011-09-21 10:22:35 标签:Android 移动开发 JNI OpenCV NDK 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始 ...

  3. 【原创】如何在Android中为TextView动态设置drawableLeft等

    如何在Android中为TextView动态设置drawableLeft等   两种方式:   方式1:手动设置固有边界 Drawable drawable = getResources().getD ...

  4. Android中的创建型模式总结

    共5种,单例模式.工厂方法模式.抽象工厂模式.建造者模式.原型模式 单例模式 定义:确保某一个类的实例只有一个,而且向其他类提供这个实例. 单例模式的使用场景:某个类的创建需要消耗大量资源,new一个 ...

  5. Kotlin 第二弹:Android 中 PDF 创建与渲染实践

    这是 Kotlin 练习的的第二篇.这一篇的由来是因为刚刚在 Android 开发者官网查看 API 的时候,偶然看到了角落里面的 pdf 相关. 我仔细看看了详细文档,发现这个还蛮有意思的,关键是编 ...

  6. 如何在VMware中进行创建CentOS虚拟机

    今天给大家分享如何在VMware中创建CentOS虚拟机,CentOS6.7为例进行说明,CentOS7版本亦可以参考该教程,具体的教程如下. 1.之后打开VMware,主页面如下图所示.点击第一个框 ...

  7. 如何在JavaScript中手动创建类数组对象

    前言 关于什么是js的类数组对象这里不再赘述.可以参考这个链接,还有这里. js中类数组对象很多,概念简单的讲就是看上去像数组,又不是数组,可以使用数字下标方式访问又没有数组方法. 例: argume ...

  8. Java中如何创建一个新的对象的/Creating Objects/

    The Java Tutorials have been written for JDK 8. Examples and practices described in this page don't ...

  9. 什么是不可变对象(immutable object)?Java 中怎么 创建一个不可变对象?

    不可变对象指对象一旦被创建,状态就不能再改变.任何修改都会创建一个新的对象,如 String.Integer 及其它包装类. 详情参见答案,一步一步指导你在 Java中创建一个不可变的类.

随机推荐

  1. CronExpression表达式详解和案例

    1. cron表达式格式: {秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)} 2. cron表达式各占位符解释: {秒数} ==> 允许值范围: 0~59 ,不允许 ...

  2. 关于async function(){ let res = await } 详解

    本文引自: https://www.jianshu.com/p/435a8b8cc7d3 async function fn(){ //表示异步,这个函数里面有异步任务 let result = aw ...

  3. JQuery iframe宽高度自适应浏览器窗口大小的解决方法

    iframe宽高度自适应浏览器窗口大小的解决方法   by:授客 QQ:1033553122 1.   测试环境 JQuery-3.2.1.min.js 下载地址: https://gitee.com ...

  4. Python 元組 Tuple

    元組 Tuple 宣告 元組是用逗號分隔的一列值: >>> t = 'a',1,'b',2,'c',3>>> t('a', 1, 'b', 2, 'c', 3)&g ...

  5. centos7.6 安装Mysql5.7

    #安装Mysqlwget http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpmyum localinstall mys ...

  6. Druid-代码段-1-3

    所属文章:池化技术(一)Druid是如何管理数据库连接的? 本代码段对应流程1.2,真正获取连接的执行: private DruidPooledConnection getConnectionInte ...

  7. juc-2-原子变量与CAS算法

      i++的原子性问题 例子   int i=10;           i++; 结果   i=10   分析过程                    在计算机 底层 会有生成一个临时变量 tem ...

  8. c++ istringstream用法

    istringstream用法,见如下代码 #include <iostream> #include"sstream" using namespace std; int ...

  9. 为什么 Java 不是纯面向对象语言?

    什么是纯面向对象语言? 纯面向对象语言或完全面向对象语言是指完全面向对象的语言,它支持或具有将程序内的所有内容视为对象的功能.它不支持原始数据类型(如 int,char,float,bool 等).编 ...

  10. 使用celery执行Django串行异步任务

    Django项目有一个耗时较长的update过程,希望在接到请求运行update过程的时候,Django应用仍能正常处理其他的请求,并且update过程要求不能并行,也不能漏掉任何一个请求 使用cel ...