首页 > 编程技术 > android

Android用MemoryFile文件类读写进行性能优化

发布时间:2016-9-20 19:58

java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。

Android匿名共享内存对外Android系统的匿名共享内存子系统的主体是以驱动程序的形式实现在内核空间的,同时在应用程序框架层提供了Java调用接口。在Android应用程序框架层,提供了一个MemoryFile接口来封装了匿名共享内存文件的创建和使用,它实现在frameworks/base/core/java/android/os/MemoryFile.java

public MemoryFile(String name, int length) throws IOException {  
   mLength = length;  
   //打开"/dev/ashmem"设备文件  
   mFD = native_open(name, length);  
   if (length > 0) {  
       //将打开的"/dev/ashmem"设备文件映射到进程虚拟地址空间中  
       mAddress = native_mmap(mFD, length, PROT_READ | PROT_WRITE);  
   } else {  
       mAddress = 0;  
   }
}  

native_open函数是一个本地函数,通过JNI实现在C++层,代码位于frameworks\base\core\jni\android_os_MemoryFile.cpp

static jobject android_os_MemoryFile_open(JNIEnv* env, jobject clazz, jstring name, jint length)  
{  
   //字符串转换  
   const char* namestr = (name ? env->GetStringUTFChars(name, NULL) : NULL);  
   //打开设备文件"/dev/ashmem",并修改设备文件名称及共享内存大小  
   int result = ashmem_create_region(namestr, length);  
   if (name)  
       env->ReleaseStringUTFChars(name, namestr);  
   if (result < 0) {  
       jniThrowException(env, "java/io/IOException", "ashmem_create_region failed");  
       return NULL;  
   }  
   //设备文件句柄转换  
   return jniCreateFileDescriptor(env, result);  
}

函数首先将Java层传过来的你们共享内存名称转换为C++层的字符串,然后调用ashmem_create_region函数创建一个名为dev/ashmem/的匿名共享内存,并且修改该共享内存的名称及大小,然后将创建的匿名共享内存设备文件句柄值返回到Java空间中。函数ashmem_create_region在Android 匿名共享内存C接口分析中有详细分析,该接口函数就是用于创建一块匿名共享内存。

在Java空间构造MemoryFile对象时,首先打开/dev/ashmem设备文件并在内核空间创建一个ashmem_area,接着需要将内核空间分配的共享内存地址映射到进程虚拟地址空间中来,映射过程是通过native_mmap函数来完成的。


static jint android_os_MemoryFile_mmap(JNIEnv* env, jobject clazz, jobject fileDescriptor,  
       jint length, jint prot)  
{  
   int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);  
   jint result = (jint)mmap(NULL, length, prot, MAP_SHARED, fd, 0);  
   if (!result)  
       jniThrowException(env, "java/io/IOException", "mmap failed");  
   return result;  
}  

该函数直接调用mmap来实现地址空间映射,注意标志位MAP_SHARED,表示该缓冲区以共享方式映射。映射过程是由Ashmem驱动来完成,Android 匿名共享内存驱动源码分析详细分析了Android匿名共享内存的实现过程。在构造MemoryFile对象时完成了匿名共享内存的创建及地址空间的映射过程,将创建的匿名共享内存的大小保存到MemoryFile的成员变量mLength中,成员变量mFD保存创建的匿名共享内存的文件描述符,成员变量mAddress保存匿名共享内存映射到进程地址空间的起始地址。有了这些信息后,就可以直接使用该匿名共享内存了。

匿名共享内存读

对匿名共享内存的读取操作,在Java空间被封装成MemoryInputStream来完成,该类继承于输入流InputStream,并对外提供了read方法,定义如下:

@Override  
public int read() throws IOException {  
   if (mSingleByte == null) {  
       mSingleByte = new byte[1];  
   }  
   int result = read(mSingleByte, 0, 1);  
   if (result != 1) {  
       return -1;  
   }  
   return mSingleByte[0];  
}  
@Override  
public int read(byte buffer[], int offset, int count) throws IOException {  
   if (offset < 0 || count < 0 || offset + count > buffer.length) {  
       // readBytes() also does this check, but we need to do it before  
       // changing count.  
       throw new IndexOutOfBoundsException();  
   }  
   count = Math.min(count, available());  
   if (count < 1) {  
       return -1;  
   }  
   int result = readBytes(buffer, mOffset, offset, count);  
   if (result > 0) {  
       mOffset += result;  
   }  
   return result;  
}  

MemoryInputStream类提供了两个read重载方法,第一个无参read方法调用有参read方法来读取1字节的数据,而有参read方法的数据读取过程是调用MemoryInputStream的外部类MemoryFile的readBytes方法来实现匿名共享内存数据的读取过程。

public int readBytes(byte[] buffer, int srcOffset, int destOffset, int count)  
       throws IOException {  
   if (isDeactivated()) {  
       throw new IOException("Can't read from deactivated memory file.");  
   }  
   if (destOffset < 0 || destOffset > buffer.length || count < 0  
           || count > buffer.length - destOffset  
           || srcOffset < 0 || srcOffset > mLength  
           || count > mLength - srcOffset) {  
       throw new IndexOutOfBoundsException();  
   }  
   return native_read(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);  
}  

该函数也仅仅作了一些判断,然后直接调用本地方法native_read在C++空间完成数据读取,在构造MemoryFile对象时,已经打开并映射了dev/ashmem设备文件,因此在这里直接将打开该设备文件得到的文件句柄值传到C++空间,以正确读取指定的匿名共享内存中的内容,mAddress为匿名共享内存映射到进程地址空间中的起始地址。

static jint android_os_MemoryFile_read(JNIEnv* env, jobject clazz,  
       jobject fileDescriptor, jint address, jbyteArray buffer, jint srcOffset, jint destOffset,  
       jint count, jboolean unpinned)  
{  
   int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);  
   if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {  
       ashmem_unpin_region(fd, 0, 0);  
       jniThrowException(env, "java/io/IOException", "ashmem region was purged");  
       return -1;  
   }  
   env->SetByteArrayRegion(buffer, destOffset, count, (const jbyte *)address + srcOffset);  
   if (unpinned) {  
       ashmem_unpin_region(fd, 0, 0);  
   }  
   return count;  
}  

匿名共享内存写

将指定数据写入到匿名共享内存中,对匿名共享内存的写操作使用MemoryOutputStream来封装,该类提供了两个重载的write方法,一个用于向匿名共享内存写入多字节数据,另一个则只写入一个字节数据。这里简单介绍多字节数据写入过程:

public void write(byte buffer[], int offset, int count) throws IOException {  
   writeBytes(buffer, offset, mOffset, count);  
   mOffset += count;  
}  
参数buffer是指写入匿名共享内存中的字节数组,offset指定数据buffer开始写的偏移量,参数count指定写入匿名共享内存的字节长度,函数调用MemoryFile的writeBytes函数来完成数据写入。

public void writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)  
       throws IOException {  
   if (isDeactivated()) {  
       throw new IOException("Can't write to deactivated memory file.");  
   }  
   if (srcOffset < 0 || srcOffset > buffer.length || count < 0  
           || count > buffer.length - srcOffset  
           || destOffset < 0 || destOffset > mLength  
           || count > mLength - destOffset) {  
       throw new IndexOutOfBoundsException();  
   }  
   native_write(mFD, mAddress, buffer, srcOffset, destOffset, count, mAllowPurging);  
}  


该函数首先检验参数的正确性,然后调用native方法native_write通过JNI转入C++完成数据写入,第一个参数是匿名共享内存的文件描述符,第二个参数是匿名共享内存映射到进程地址空间的基地值,后面三个参数上面已经介绍了,最后一个参数mAllowPurging表示是否允许内存回收

描述了匿名共享内存的写入过程,本质上就是将buffer中指定位置开始的数据拷贝到匿名共享内存指定的偏移位置
frameworks\base\core\jni\android_os_MemoryFile.cpp

static jint android_os_MemoryFile_write(JNIEnv* env, jobject clazz,  
       jobject fileDescriptor, jint address, jbyteArray buffer, jint srcOffset, jint destOffset,  
       jint count, jboolean unpinned)  
{  
   int fd = jniGetFDFromFileDescriptor(env, fileDescriptor);  
   if (unpinned && ashmem_pin_region(fd, 0, 0) == ASHMEM_WAS_PURGED) {  
       ashmem_unpin_region(fd, 0, 0);  
       jniThrowException(env, "java/io/IOException", "ashmem region was purged");  
       return -1;  
   }  
   env->GetByteArrayRegion(buffer, srcOffset, count, (jbyte *)address + destOffset);  
   if (unpinned) {  
       ashmem_unpin_region(fd, 0, 0);  
   }  
   return count;  
}  

数据写入过程是通过JNI函数GetByteArrayRegion完成数据的拷贝操作。

MemoryFile主要的构造方法 MemoryFile(String name, int length) ,这里第二个参数为文件大小,需要说明的是Android的MemoryFile和传统的mmap还有一点点区别,毕竟是手机,它内部的内存管理方式ashmem会从内核中回收资源。毕竟目前部分低端机型的RAM也比较吃紧。

synchronized boolean  allowPurging(boolean allowPurging)  //允许ashmem清理内存,线程安全同步的方式。

void  close() //关闭,因为在Linux内部mmap占用一个句柄,不用时一定要释放了

InputStream  getInputStream()  返回读取的内容用Java层的InputStream保存

OutputStream  getOutputStream()  把一个OutputSream写入到MemoryFile中

boolean  isPurgingAllowed() //判断是否允许清理

int  length()  //返回内存映射文件大小

下面就是我们熟悉的,读写细节,主要是对字符数组的操作,这里大家要计算好每个文件类型的占用,同时考虑到效率对于自己分配的大小考虑粒度对齐。

int  readBytes(byte[] buffer, int srcOffset, int destOffset, int count)

void  writeBytes(byte[] buffer, int srcOffset, int destOffset, int count)


应用场合:对于I/O需要频繁操作的,主要是和外部存储相关的I/O操作,MemoryFile通过将 NAND或SD卡上的文件,分段映射到内存中进行修改处理,这样就用高速的RAM代替了ROM或SD卡,性能自然提高不少,对于Android手机而言同时还减少了电量消耗。

Android开发,Serializable,Parcelable

两种都是用于支持序列化、反序列化话操作,两者最大的区别在于存储媒介的不同,Serializable使用IO读写存储在硬盘上,而Parcelable是直接在内存中读写,很明显内存的读写速度通常大于IO读写,所以在Android中通常优先选择Parcelable。

Serializable不是当前关注的焦点,不过可以查看《Java序列化算法透析》这篇文章中实现一个简单的Serializable例子,查看序列化生成的IO文件,并且以16进制读取并一一解释每一个16进制数字的含义。


1、作用

Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流、rmi以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。

从上面的设计上我们就可以看出优劣了。


2、效率及选择

Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化
 

3、编程实现

对于Serializable,类只需要实现Serializable接口,并提供一个序列化版本id(serialVersionUID)即可。而Parcelable则需要实现writeToParcel、describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。

Parcelable的一个实现例子如下

public class MyParcelable implements Parcelable {
     private int mData;
     private String mStr;

     public int describeContents() {
         return 0;
     }

     // 写数据进行保存
     public void writeToParcel(Parcel out, int flags) {
         out.writeInt(mData);
         out.writeString(mStr);
     }

     // 用来创建自定义的Parcelable的对象
     public static final Parcelable.Creator<MyParcelable> CREATOR
             = new Parcelable.Creator<MyParcelable>() {
         public MyParcelable createFromParcel(Parcel in) {
             return new MyParcelable(in);
         }

         public MyParcelable[] newArray(int size) {
             return new MyParcelable[size];
         }
     };
     
     // 读数据进行恢复
     private MyParcelable(Parcel in) {
         mData = in.readInt();
         mStr = in.readString();
     }
 }

从上面我们可以看出Parcel的写入和读出顺序是一致的。如果元素是list读出时需要先new一个ArrayList传入,否则会报空指针异常。如下:

list = new ArrayList<String>();
in.readStringList(list);

 PS: 在自己使用时,read数据时误将前面int数据当作long读出,结果后面的顺序错乱,报如下异常,当类字段较多时务必保持写入和读取的类型及顺序一致。

11-21 20:14:10.317: E/AndroidRuntime(21114): Caused by: java.lang.RuntimeException: Parcel android.os.Parcel@4126ed60: Unmarshalling unknown type code 3014773 at offset 164
 

4、高级功能上

Serializable序列化不保存静态变量,可以使用Transient关键字对部分字段不进行序列化,也可以覆盖writeObject、readObject方法以实现序列化过程自定义

手机应用中交互表单经常会用到多选,本文我们来看看android应用开发中如何在ListVie中实现多选的办法。

必须自己实现baseAdapter吗?如果提供了多选的choice_mode,为什么会没有提供简单的实现呢。。

问题:

使用ListView实现多选,要求数据源使用List<T>,尝试使用SimpleAdapter配和自己定义的checkBox控件无法获得选中项,在非继承ListActivity的类中自己实现一个Adapter比较麻烦。

回答:

通过ArrayAdapter使用android.R.layout.simple_list_item_multiple_choice来实现多选必须有几个限制条件:
1.所在类必须继承ListActivity
2.必须使用内部定义的ListView的id,即ListView控件的id属性必须为 android:id=”@id/android:list”
(需要注意的是存在多个ListView需要使用@id/android:list,而非@+id/android:list或者@android:id/list)
3.ListView对象通过getListView()方法获得。

ps:
使用ArrayAdapter的simple_multiple_choice解决多选从成本上来说比较小,如果数据源是cursor的可能更方便一点,但是现在数据源是List<T>,从样式上来会比较固定,需要T类自己实现toString()方法控制输出文案。

下面来为各位介绍一个安卓4.4上实现透明导航栏和状态栏 Translucent system bar例子,希望本文章对各位会带来帮助.
1、前言

 

在android kitkat 有一个新的特性可以设置手机状态栏的背景,让手机整个界面的风格保持一致,看起来非常清爽,在今年的google i/o上的android l默认就是这种风格。来现在看我们怎么加上这个酷黑狂拽掉渣天的功能怎么给我们的程序加上。一。

 

 
2、关与kitkat

 

android 4.4 提供了一套能透明的系统ui样式给状态栏和导航栏,这样的话就不用向以前那样每天面对着黑乎乎的上下两条黑栏了,还可以调成跟activity 一样的样式,形成一个完整的主题。

 

3、设置方法

 

首先要打开activity的透明主题功能,可以把activity的主题设置继承*.TranslucentDecor 主题,然后设置android:windowTranslucentNavigation 或者android:windowTranslucentStatus的主题属性为true,又或者在activity的代码里面开启FLAG_TRANSLUCENT_NAVIGATION 或是 FLAG_TRANSLUCENT_STATUS的window窗口标识。由于透明主题不能在4.4以前的版本里面使用,所以系统样式跟以前没有区别,也就是看不到任何变化,这是一个兼容模式,这个模式可以兼容到api 10.

激活主题

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    // 创建状态栏的管理实例
    SystemBarTintManager tintManager = new SystemBarTintManager(this);
    // 激活状态栏设置
    tintManager.setStatusBarTintEnabled(true);
    // 激活导航栏设置
    tintManager.setNavigationBarTintEnabled(true);
}

设置状态栏颜色和图片

 

// 设置一个颜色给系统栏
tintManager.setTintColor(Color.parseColor("#99000FF"));
// 设置一个样式背景给导航栏
tintManager.setNavigationBarTintResource(R.drawable.my_tint);
// 设置一个状态栏资源
tintManager.setStatusBarTintDrawable(MyDrawable);
4、开源下载地址

ART 将会取代Dalvik虚拟机,因为 在Dalvik下,应用每次运行的时候,字节码都需要通过即时编译器转换为机器码,这会拖慢应用的运行效率,而在ART 环境中,应用在第一次安装的时候,字节码就会预先编译成机器码,使其成为真正的本地应用。

在最新的Google I/O大会上,Google 发布了关于Android上最新的运行时库的情况。这就是Android RunTime (ART). ART 将会取代Dalvik虚拟机,成为Android平台上Java代码的执行工具。虽然自从Android KitKat,就有了一些关于ART的消息,但是基本都是一些新闻性质的,缺乏具体技术细节方面的介绍。本文尝试综合目前已有的各种消息,以及最新放出的 Android L 预览版本的ROM的情况,对ART运行时库做个详细的分析。

 

ART 特性简介

 

和IOS,Windows,Tizen之类的移动平台直接将软件编译成能够直接运行在特定硬件平台上的本地代码不同。Android平台上的软件会被编译器首先编译成通用的“byte-code”,然后再在具体的移动设备上被转换成本地指令执行。

从Android诞生至今的十几年时间里,Dalvik从开始时非常简单的Java Byte-Code执行虚拟机,逐渐增加各种新的特性,满足应用程序对性能的需求,以及与硬件设备协同演进。这其中包括在Android 2.2版本中引入的即时编译器(JIT-Compiler), 以及随后的多线程支持,以及其他一些优化。

 

Android平台的演进

 

不过,在近两年里,Android整个生态系统的进步对Android虚拟机的需求,目前的Dalvik虚拟机的开发已经无法满足了。Dalvik 最初设计时,处理器的性能很弱,移动设备的内存空间非常有限,而且都是32位的系统。于是Google开始构建一个新的虚拟机来更好的面对未来的发展趋势。这种虚拟机的性能能够在目前的多核处理器,甚至未来的8核处理上轻松扩展,能够满足对大容量存储的支持,以及大容量内存的支持。 于是乎,ART出现了。

1 架构介绍

 

APK文件的工作流

 

首先,ART的首要设计需求就是完全兼容能在Dalvik上运行的byte-code,即dex(Dalvik executable)。这样的话,对于程序员来说,就不需要对重新编译已有的程序,直接拿APK就可以在Dalvik和ART虚拟机上运行。ART带来的最大的变化,就是使用预编译技术(Ahead-of-Time compile)取代Dalvik中的即时编译技术(Just-In-Time compile)。之前,在应用程序每次执行的时候,虚拟机需要将bytecode编译成本地码执行,而在ART中这种编译操作只需执行一次,随后对该应用程序的执行都可以通过直接执行保存下来的本地码完成。当然,这种预编译技术,需要占用额外的存储空间来存储本地码。正是因为现在移动设备的存储空间越来越大,这种技术才得以应用。

这种预编译技术使得很多原来无法执行的编译优化技术在新的Android平台上成为可能。因为代码只被编译和优化一次,因此值得花费更多的时间在这次编译上,以便进行更多的优化。Google表示,现在可以在应用程序的整体代码技术上进行更高层次的优化,因为编译器现在能够看到应用程序的整体代码,而之前的即时编译,编译器只能看到并优化应用程序中某个函数或者非常小的一部分代码。采用ART后,代码中异常检查带来的开销绝大部分可以避免,对方法和接口的调用也加快了很多。完成这部分功能的是新添加的“dex2oat”组件,用来替代Dalvik中对应的“dexopt”组件。Dalvik中的 Odex文件(优化后的dex)文件,在ART中也用ELF文件代替了。

因为ART目前编译生成ELF可执行文件,内核就可以直接对载入内存中的代码进行分页管理,这也会带来更加高效的内存管理,以及更少的内存占用。说到这里,我非常好奇内核中的KSM(Kernel same-page merging)在ART中会有什么样的影响,应该能带来不错的效果吧。我们拭目以待。

ART对续航时间的影响也是非常显著的。因为不再需要解释执行,JIT也不用在程序运行时工作,这样会直接节省CPU需要执行的指令数,因而耗电降低。

因为预编译时引入了更多分析和优化,编译的时间会变长,这是ART可能会带来的一个副作用。因此相比Dalvik虚拟机,当设备首次启动及应用程序第一次安装时,需要花费的时间更久。Google声称,这种时间上的增加并不那么恐怖。他们希望并预期日后ART上完成上述动作的时间会和目前的 Dalvik差不多,甚至更短些。

 

ART与Dalvik性能比较

 

上面的图显示,ART带来的性能提升是非常明显的。对于同样的代码,性能提升约2倍左右。Google称,将Android L最终发布的时候,可以预计的性能提升将会像Chessbench一样,有3x的加速。

2 垃圾收集:理论和实践

Android虚拟机依赖自动化的内存管理机制,即自动垃圾收集。这一Java语言编程模式的基石也是Android系统自诞生之日起,非常重要的一部分。这里向不太了解垃圾收集概念的朋友解释一下,所谓自动垃圾收集,就是说程序员在编程过程中,不需要自己负责物理内存的存储的分配和释放。只需要使用固定的模式创建你需要的变量或者对象,然后直接利用该变量或对象即可。程序的运行环境会自动在内存中分配相应的内存空间存储该变量或者对象, 并在该变量或者对象失效后,自动释放所分配的内存。这是和其他需要人工进行存储管理的较低层次语言最大的区别。自动垃圾收集的好处是,程序员不必再在编程时担心内存管理的问题,当然,这也是有代价的,那就是程序员无法控制内存何时分配和释放,因而无法在需要时进行优化(Java语言有一些编程接口可以供程序员手工优化程序,但控制方式和粒度有限).

Android曾经被Dalvik的垃圾收集机制折腾了很久。Android平台的内存普遍较小,每次应用程序需要分配内存,当堆空间(分配给应用程序的一块内存空间)不能提供如此大小的空间时,Dalvik的垃圾收集器就会启动。垃圾收集器会遍历整个堆空间,查看每一个应用程序分配的对象,并对所有可到达的对象(即还会被使用的对象)标记,并将那些没有标记的对象空间释放掉。

在Dalvik虚拟机中,垃圾收集器执行的过程将导致两次应用程序的停顿:

一是在遍历堆地址空间阶段,

另一个是标记阶段。

所谓停顿,即应用程序所有正在执行的进程将暂停。如果停顿时间过长,将会导致应用程序在渲染时出现丢帧现象,进而导致应用程序的卡顿现象,大大降低用户体验。

Google声称,在Nexus 5手机上,这种停顿的平均长度在54ms。这个停顿时间将导致平均每次垃圾收集会导致在应用程序渲染显式时丢掉4帧的。

我自己的经验和测试表明,根据应用程序的不同,停顿的时间可能会增大很多。比如,在官方的FIFA应用程序这一典型程序中,垃圾收集的停顿会非常厉害。

07-01 15:56:14.275: D/dalvikvm(30615): GCFORALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms, total 24ms

07-01 15:56:16.785: I/dalvikvm-heap(30615): Grow heap (frag case) to 38.179MB for 8294416-byte allocation

07-01 15:56:17.225: I/dalvikvm-heap(30615): Grow heap (frag case) to 48.279MB for 7361296-byte allocation

07-01 15:56:17.625: I/Choreographer(30615): Skipped 35 frames! The application may be doing too much work on its main thread.

07-01 15:56:19.035: D/dalvikvm(30615): GCCONCURRENT freed 35838K, 43% free 51351K/89052K, paused 3ms+5ms, total 106ms

07-01 15:56:19.035: D/dalvikvm(30615): WAITFORCONCURRENTGC blocked 96ms

07-01 15:56:19.815: D/dalvikvm(30615): GCCONCURRENT freed 7078K, 42% free 52464K/89052K, paused 14ms+4ms, total 96ms

07-01 15:56:19.815: D/dalvikvm(30615): WAITFORCONCURRENTGC blocked 74ms

07-01 15:56:20.035: I/Choreographer(30615): Skipped 141 frames! The application may be doing too much work on its main thread.

07-01 15:56:20.275: D/dalvikvm(30615): GCFORALLOC freed 4774K, 45% free 49801K/89052K, paused 168ms, total 168ms

07-01 15:56:20.295: I/dalvikvm-heap(30615): Grow heap (frag case) to 56.900MB for 4665616-byte allocation

07-01 15:56:21.315: D/dalvikvm(30615): GCFORALLOC freed 1359K, 42% free 55045K/93612K, paused 95ms, total 95ms

07-01 15:56:21.965: D/dalvikvm(30615): GCCONCURRENT freed 6376K, 40% free 56861K/93612K, paused 16ms+8ms, total 126ms

07-01 15:56:21.965: D/dalvikvm(30615): WAITFORCONCURRENTGC blocked 111ms

07-01 15:56:21.965: D/dalvikvm(30615): WAITFORCONCURRENTGC blocked 97ms

07-01 15:56:22.085: I/Choreographer(30615): Skipped 38 frames! The application may be doing too much work on its main thread.

07-01 15:56:22.195: D/dalvikvm(30615): GCFORALLOC freed 1539K, 40% free 56833K/93612K, paused 87ms, total 87ms

07-01 15:56:22.195: I/dalvikvm-heap(30615): Grow heap (frag case) to 60.588MB for 1331732-byte allocation

07-01 15:56:22.475: D/dalvikvm(30615): GCFORALLOC freed 308K, 39% free 59497K/96216K, paused 84ms, total 84ms

07-01 15:56:22.815: D/dalvikvm(30615): GCFORALLOC freed 287K, 38% free 60878K/97516K, paused 95ms, total 95ms

上面的log是从FIFA应用程序运行后的几秒钟时间里截取的。垃圾收集器在短短的8秒内被执行了9次,导致应用程序总共卡顿了603ms,丢帧达214次。绝大多数的卡顿都来自内存分配请求,在log中以”GC_FOR_ALLOC“标签描述。

ART将整个垃圾收集系统做了重新设计和实现。为了能做些对比,下面给出使用ART运行相同的应用程序,在相同的场景下提取的log:

07-01 16:00:44.531: I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms

07-01 16:00:44.545: I/art(198): Explicit concurrent mark sweep GC freed 7(240B) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 198us total 9.465ms

07-01 16:00:44.554: I/art(198): Explicit concurrent mark sweep GC freed 5(160B) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 224us total 9.045ms

07-01 16:00:44.690: I/art(801): Explicit concurrent mark sweep GC freed 65595(3MB) AllocSpace objects, 9(4MB) LOS objects, 810% free, 38MB/58MB, paused 1.195ms total 87.219ms

07-01 16:00:46.517: I/art(29197): Background partial concurrent mark sweep GC freed 74626(3MB) AllocSpace objects, 39(4MB) LOS objects, 1496% free, 25MB/32MB, paused 4.422ms total 1.371747s

07-01 16:00:48.534: I/Choreographer(29197): Skipped 30 frames! The application may be doing too much work on its main thread.

07-01 16:00:48.566: I/art(29197): Background sticky concurrent mark sweep GC freed 70319(3MB) AllocSpace objects, 59(5MB) LOS objects, 825% free, 49MB/56MB, paused 6.139ms total 52.868ms

07-01 16:00:49.282: I/Choreographer(29197): Skipped 33 frames! The application may be doing too much work on its main thread.

07-01 16:00:49.652: I/art(1287): Heap transition to ProcessStateJankImperceptible took 45.636146ms saved at least 723KB

07-01 16:00:49.660: I/art(1256): Heap transition to ProcessStateJankImperceptible took 52.650677ms saved at least 966KB

ART和Dalvik的差别非常大,新的运行时内存管理仅仅停顿了12.364ms,运行了4次前台垃圾收集,以及2次后台垃圾收集。在应用程序执行的过程中,应用程序的堆空间大小并没有增加,而Dalvik虚拟机中堆空间共增加了4次。丢帧的个数方面,ART虚拟机也降到了63帧。

上面这段示例,只不过是一个开发并不完善的应用程序中最坏的一个场景。因为即使在ART虚拟机中,这个应用程序还是丢掉了不少帧渲染图像。不过上面的log对比依然很有参考价值,毕竟牛逼的程序员没几个,大多数的Android程序都没办法开发的很完美。Android需要能hold住这种情况。

ART将一些通常需要垃圾收集器做的工作,拆分给应用程序本身完成。这样,Dalvik中因为遍历堆空间引入的第一次停顿,就被完全消除了。而第二次停顿也因为一项预清理技术 (packard pre-cleaning)的应用而大大缩短。使用该技术后,只需要在清理完成后,简单的检查和验证时稍微停顿一下即可。Google声称,他们已经设法将这类停顿的时间缩短到3ms左右,相比Dalvik虚拟机的垃圾收集器来说,基本上是一个多数量级的降低,很不错了。

 

垃圾收集性能对比

 

ART还引入了一个特殊的超大对象存储空间(large object space,LOS),这个空间与堆空间是分开的,不过仍然驻留在应用程序内存空间中。这一特殊的设计是为了让ART可以更好的管理较大的对象,比如位图对象(bitmaps)。在对堆空间分段时,这种较大的对象会带来一些问题。比如,在分配一个此类对象时,相比其他普通对象,会导致垃圾收集器启动的次数增加很多。有了这个超大对象存储空间的支持,垃圾收集器因堆空间分段而引发调用次数将会大大降低,这样垃圾收集器就能做更加合理的内存分配,从而降低运行时开销。

一个很好的例子,就是运行Hangouts(环聊)应用程序时,在Dalvik虚拟机中,我们能看到数次因为分配内存,运行GC而导致的停顿。

07-01 06:37:13.481: D/dalvikvm(7403): GCEXPLICIT freed 2315K, 46% free 18483K/34016K, paused 3ms+4ms, total 40ms

07-01 06:37:13.901: D/dalvikvm(9871): GCCONCURRENT freed 3779K, 22% free 21193K/26856K, paused 3ms+3ms, total 36ms

07-01 06:37:14.041: D/dalvikvm(9871): GCFORALLOC freed 368K, 21% free 21451K/26856K, paused 25ms, total 25ms

07-01 06:37:14.041: I/dalvikvm-heap(9871): Grow heap (frag case) to 24.907MB for 147472-byte allocation

07-01 06:37:14.071: D/dalvikvm(9871): GCFORALLOC freed 4K, 20% free 22167K/27596K, paused 25ms, total 25ms

07-01 06:37:14.111: D/dalvikvm(9871): GCFORALLOC freed 9K, 19% free 23892K/29372K, paused 27ms, total 28ms

我们从所有的垃圾收集log中截取了上述一段。其中的显式(GC_EXPLICIT)和并发(GC_CONCURRENT)是垃圾收集器中比较通用的清理和维护性调用。GC_FOR_ALLOC则是在内存分配器尝试分配新的内存空间,但堆空间不够用时,调用的。上面的log中,我们能看到堆空间因为分段操作而扩充了堆空间,但仍然无法装下大对象。在整个大对象分配的过程中,停顿时间长达90ms。

相比之下,下面这段log是从Android L预览版本的ART运行log中提取的。

07-01 06:35:19.718: I/art(10844): Heap transition to ProcessStateJankPerceptible took 17.989063ms saved at least -138KB

07-01 06:35:24.171: I/art(1256): Heap transition to ProcessStateJankImperceptible took 42.936250ms saved at least 258KB

07-01 06:35:24.806: I/art(801): Explicit concurrent mark sweep GC freed 85790(3MB) AllocSpace objects, 4(10MB) LOS objects, 850% free, 35MB/56MB, paused 961us total 83.110ms

我们目前还不知道log中的”Heap Transition”表达的什么意思,不过可以猜测应该是堆空间大小重设机制。在应用程序已经运行之后,唯一的对垃圾收集器的调用仅消耗的961us。我们并没有在这段截取的log之前,发现任何对垃圾收集器的调用操作。这段log中比较有趣的,就是LOS的统计。能够看到,在LOS中有4个较大的对象,共10MB。这块内存并没有分配在堆空间内,否则应该会有类似Dalvik的提示。

ART的内存分配系统本身也被重写了。虽然ART相比Dalvik,在内存分配方面,能够带来大约25%的性能提升,不过Google显然对此不满意,因此引入了一个新的内存分配器来取代当前使用的“malloc”分配器。

这个新的内存分配器,“rosalloc”(Runs-of-Slots-Allocator)是依据多线程Java应用程序的特点而设计的。此内存分配器有更细粒度的锁机制,可以直接对独立的对象上锁,而非对整个待分配的内存空间上锁。在线程局部区域中的小对象的分配,完全可以无视锁的存在了。没有了锁的请求和释放,线程局部小对象的访问速度也就大幅提升了。

这个新的内存分配器大幅提升了内存分配的速度,加速比达到了10x。

同时,ART的垃圾回收算法也做了改进,提升了用户使用体验,避免应用程序的卡顿。这些算法在Google内部目前仍然正在开发中。近期,Google仅仅介绍了一个新算法,“Moving Garbage Collector”.核心思想是,当应用程序运行在后台时,将程序的堆空间做段合并操作。

3 64位支持

ART在设计时充分考虑了将日后可能运行的各种平台进行模块化。因此,ART提供了大量的编译器后端,用于生成目前常见的体系结构的代码,例如ARM,X86和MIPS,其中包括对ARM64, X86-64的支持,以及尚未实现的对MIPS64的支持。

 

64位Android系统的优势

 

对于ARM的64位系统带来的好处,相比很多朋友都了解了。更大的内存地址空间,普适的性能提升,以及加解密的能力和性能提升,此外还有对已有32位应用程序的兼容。

除此之外,Google还在ART中引入了引用压缩技术,来避免ART堆空间内部因为64位指针的引入导致的内存占用变大问题。其实,就是在执行时,所有的指针都采用32位表示,而非64位系统应该采用的64位指针。

 

64位平台性能提升

 

Google公开了一些ARM和X86平台上应用程序在64位和32位模式下的性能对比。这只是一些预览性质数据。X86的性能测试在Intel的 BayTrail系统上进行,对于不同的RenderScript测试程序,性能提升从2x到4.5x不等。ARM平台方面,分别在A57和A53系统上,对crypto的性能做了对比。这些数据因为都是针对非常小的例子,所以代表性不大,因此还无法代表实际应用场景的情况。

不过,Google也放出了一些有趣的数据,这些数据是在他们内部使用的系统Panorama上测试的。通过简单的从32位ABI转换为64位 ABI,能够获得13%到19%的性能提升。还有个喜人的结论,那就是ARM的Cortex A53在AArch64模式下能获得性能提升比A57核要多。

Google还声称,目前应用商店中85%的应用程序都可以直接在64位模式下运行,也就是说仅有15%的应用程序在某种程度上使用了本地代码,需要重新为64位平台编译该应用程序。这对Google来说将是一个非常大的优势。明年,当大多数芯片厂商都开始推64位片上系统的时候,从32位 Android系统到64位Android系统的的切换将会非常快。

4 结论

结合上面介绍的诸多方面,ART是Google发布的一款性能提升大杀器,并且ART也解决了多个数年来困扰Android系统的诸多问题。ART 有效地改进了多个解释执行应用程序面临的问题,也提供了一个自动化的高效的存储管理系统。对于开发者来说,许多过去需要手工添加代码解决的性能问题,现在都能被ART轻松hold住了。

这也意味着Android系统终于能够在系统平滑度,应用程序性能方面与IOS势均力敌了。对消费者来说,是件喜大普奔的事情。

Google目前仍在,而且在未来一段时间内还将大力改进ART。ART目前的状况,与6个月前已经大不相同了,预计等到Android L真正发布的时候,又会有翻天覆地的变化。前途是光明的,让我们拭目以待,翘首期盼吧。

标签:[!--infotagslink--]

您可能感兴趣的文章: