1、即时编译(Just-in-time Compilation,JIT),又称动态转译(Dynamic Translation),是一种通过在运行时将字节码翻译为机器码,从而改善字节码编译语言性能的技术。即时编译前期的两个运行时理论是字节码编译和动态编译。Android原来Dalvik虚拟机是作为一种解释器实现,新版(Android2.2+)将换成JIT编译器实现。性能测试显示,在多项测试中新版本比旧版本提升了大约6倍。
2、
就像世界上没有免费的午餐,世界上也没有免费的对象。虽然gc为每个线程都建立了临时对象池,可以使创建对象的代价变得小一些,但是分配内存永远都比不分配内存的代价大。如果你在用户界面循环中分配对象内存,就会引发周期性的垃圾回收,用户就会觉得界面像打嗝一样一顿一顿的。所以,除非必要,应尽量避免尽力对象的实例。下面的例子将帮助你理解这条原则:当你从用户输入的数据中截取一段字符串时,尽量使用substring函数取得原始数据的一个子串,而不是为子串另外建立一份拷贝。这样你就有一 个新的String对象,它与原始数据共享一个char数组。 如果你有一个函数返回一个String对象,而你确切的知道这个字符串会被附加到一个StringBuffer,那么,请改变这个函数的参数和实现方式, 直接把结果附加到StringBuffer中,而不要再建立一个短命的临时对象。
一个更极端的例子是,把多维数组分成多个一维数组:
int数组比Integer数组好,这也概括了一个基本事实,两个平行的int数组比 (int,int)对象数组性能要好很多。同理,这试用于所有基本类型的组合。如果你想用一种容器存储(Foo,Bar)元组,尝试使用两个单独的 Foo[]数组和Bar[]数组,一定比(Foo,Bar)数组效率更高。(也有例外的情况,就是当你建立一个API,让别人调用它的时候。这时候你要注重对API接口的设计而牺牲一点儿速度。当然在API的内部,你仍要尽可能的提高代码的效率)
总体来说,就是避免创建短命的临时对象。减少对象的创建就能减少垃圾收集,进而减少对用户体验的影响。
3、 静态方法代替虚拟方法
如果不需要访问某对象的字段,将方法设置为静态,调用会加速15%到20%。这也是一种好的做法,因为你可以从方法声明中看出调用该方法不需要更新此对象的状态。
4、避免内部Getters/Setters
5、 将成员缓存到本地
访问成员变量比访问本地变量慢得多,下面一段代码:
<span style="margin: 0px; padding: 0px; color: rgb(153, 51, 0);">for(int i =0; i <this.mCount; i++) {dumpItem(this.mItems);}</span>
最好改成这样:
<span style="margin: 0px; padding: 0px; color: rgb(153, 51, 0);">int count = this.mCount;Item[] items = this.mItems;for(int i =0; i < count; i++) { dumpItems(items);}</span>
另一个相似的原则是:永远不要在for的第二个条件中调用任何方法。如下面方法所示,在每次循环的时候都会调用getCount()方法,这样做比你在一个int先把结果保存起来开销大很多。
同样如果你要多次访问一个变量,也最好先为它建立一个本地变量,例如:
这里有4次访问成员变量mScrollBar,如果将它缓存到本地,4次成员变量访问就会变成4次效率更高的栈变量访问。
另外就是方法的参数与本地变量的效率相同。
6、对常量使用static final修饰符让我们来看看这两段在类前面的声明:
<span style="margin: 0px; padding: 0px; color: rgb(153, 51, 0);">static int intVal = 42;static String strVal = "Hello, world!"</span>
必以其会生成一个叫做clinit的初始化类的方法,当类第一次被使用的时候这个方法会被执行。方法会将42赋给intVal,然后把一个指向类中常量表 的引用赋给strVal。当以后要用到这些值的时候,会在成员变量表中查找到他们。 下面我们做些改进,使用“final”关键字:
static final int intVal = 42;static final String strVal = "Hello, world!";现在,类不再需要clinit方法,因为在成员变量初始化的时候,会将常量直接保存到类文件中。用到intVal的代码被直接替换成42,而使用strVal的会指向一个字符串常量,而不是使用成员变量。
将一个方法或类声明为final不会带来性能的提升,但是会帮助编译器优化代码。举例说,如果编译器知道一个getter方法不会被重载,那么编译器会对其采用内联调用。
你也可以将本地变量声明为final,同样,这也不会带来性能的提升。使用“final”只能使本地变量看起来更清晰些(但是也有些时候这是必须的,比如在使用匿名内部类的时候)。
7、
使用改进的For循环语法
改进for循环(有时被称为for-each循环)能够用于实现了iterable接口的集合类及数组中。在集合类中,迭代器让接口调用 hasNext()和next()方法。在ArrayList中,手写的计数循环迭代要快3倍(无论有没有JIT),但其他集合类中,改进的for循环语 法和迭代器具有相同的效率。下面展示集中访问数组的方法:
<span style="margin: 0px; padding: 0px; color: rgb(153, 51, 0);">static class Foo { int mSplat; } Foo[] mArray = ... public void zero() { int sum = 0; for (int i = 0; i < mArray.length; ++i) { sum += mArray[i].mSplat; } } public void one() { int sum = 0; Foo[] localArray = mArray; int len = localArray.length; for (int i = 0; i < len; ++i) { sum += localArray[i].mSplat; } } public void two() { int sum = 0; for (Foo a : mArray) { sum += a.mSplat; }}} </span>
在zero()中,每次循环都会访问两次静态成员变量,取得一次数组的长度。
在one()中,将所有成员变量存储到本地变量。
two()使用了在java1.5中引入的foreach语法。编译器会将对数组的引用和数组的长度保存到本地变量中,这对访问数组元素非常好。 但是编译器还会在每次循环中产生一个额外的对本地变量的存储操作(对变量a的存取)这样会比one()多出4个字节,速度要稍微慢一些。
8、
避免使用浮点数
通常的经验是,在Android设备中,浮点数会比整型慢两倍,在缺少FPU和JIT的G1上对比有FPU和JIT的Nexus One中确实如此(两种设备间算术运算的绝对速度差大约是10倍)从速度方面说,在现代硬件上,float和double之间没有任何不同。更广泛的讲,double大2倍。在台式机上,由于不存在空间问题,double的优先级高于float。但即使是整型,有的芯片拥有硬件乘法,却缺少除法。这种情况下,整型除法和求模运算是通过软件实现的,就像当你设计Hash表,或是做大量的算术那样,例如a/2可以换成a*0.5。
9、
减少不必要的全局变量
尽量避免static成员变量引用资源耗费过多的实例,比如Context。Android提供了很健全的消息传递机制(Intent)和任务模型(Handler),可以通过传递或事件的方式,防止一些不必要的全局变量
10、
了解Java四种引用方式
JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。
i. 强引用(StrongReference)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
ii. 软引用(SoftReference)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
iii. 弱引用(WeakReference)
在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
iv. 虚引用(PhantomReference)
顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。了解并熟练掌握这4中引用方式,选择合适的对象应用方式,对内存的回收是很有帮助的。
详细请参考 http://blog.csdn.net/feng88724/article/details/6590064
Android中常使用缓存:
a. 线程池
b. Android图片缓存,Android图片Sdcard缓存,数据预取缓存
c. 消息缓存
通过handler.obtainMessage复用之前的message,如下:
d. ListView缓存
e. 网络缓存
数据库缓存http response,根据http头信息中的Cache-Control域确定缓存过期时间。
f. 文件IO缓存
使用具有缓存策略的输入流,BufferedInputStream替代InputStream,BufferedReader替代Reader,BufferedReader替代BufferedInputStream.对文件、网络IO皆适用。
g. layout缓存
h. 其他需要频繁访问或访问一次消耗较大的数据缓存
(2). 数据存储优化
包括数据类型、数据结构的选择。
a. 数据类型选择
字符串拼接用StringBuilder代替String,在非并发情况下用StringBuilder代替StringBuffer。如果你对字符串的长度有大致了解,如100字符左右,可以直接new StringBuilder(128)指定初始大小,减少空间不够时的再次分配。
64位类型如long double的处理比32位如int慢
使用SoftReference、WeakReference相对正常的强应用来说更有利于系统垃圾回收
final类型存储在常量区中读取效率更高
LocalBroadcastManager代替普通BroadcastReceiver,效率和安全性都更高
b. 数据结构选择
常见的数据结构选择如:
ArrayList和LinkedList的选择,ArrayList根据index取值更快,LinkedList更占内存、随机插入删除更快速、扩容效率更高。一般推荐ArrayList。
ArrayList、HashMap、LinkedHashMap、HashSet的选择,hash系列数据结构查询速度更优,ArrayList存储有序元素,HashMap为键值对数据结构,LinkedHashMap可以记住加入次序的hashMap,HashSet不允许重复元素。
HashMap、WeakHashMap选择,WeakHashMap中元素可在适当时候被系统垃圾回收器自动回收,所以适合在内存紧张型中使用。
Collections.synchronizedMap和ConcurrentHashMap的选择,ConcurrentHashMap为细分锁,锁粒度更小,并发性能更优。Collections.synchronizedMap为对象锁,自己添加函数进行锁控制更方便。
Android也提供了一些性能更优的数据类型,如SparseArray、SparseBooleanArray、SparseIntArray、Pair。
Sparse系列的数据结构是为key为int情况的特殊处理,采用二分查找及简单的数组存储,加上不需要泛型转换的开销,相对Map来说性能更优。不过我不太明白为啥默认的容量大小是10,是做过数据统计吗,还是说现在的内存优化不需要考虑这些东西,写16会死吗,还是建议大家根据自己可能的容量设置初始值。
(3). 算法优化
这个主题比较大,需要具体问题具体分析,尽量不用O(n*n)时间复杂度以上的算法,必要时候可用空间换时间。
查询考虑hash和二分,尽量不用递归。可以从结构之法 算法之道或微软、Google等面试题学习。
(4). JNI
Android应用程序大都通过Java开发,需要Dalvik的JIT编译器将Java字节码转换成本地代码运行,而本地代码可以直接由设备管理器直接执行,节省了中间步骤,所以执行速度更快。不过需要注意从Java空间切换到本地空间需要开销,同时JIT编译器也能生成优化的本地代码,所以糟糕的本地代码不一定性能更优。
这个优化点会在后面单独用一片博客介绍。
(5). 逻辑优化
这个不同于算法,主要是理清程序逻辑,减少不必要的操作。
(6). 需求优化
这个就不说了,对于sb的需求可能带来的性能问题,只能说做为一个合格的程序员不能只是执行者,要学会说NO。不过不能拿这种接口敷衍产品经理哦。
2、异步,利用多线程提高TPS
充分利用多核Cpu优势,利用线程解决密集型计算、IO、网络等操作。
关于多线程可参考:
在Android应用程序中由于系统ANR的限制,将可能造成主线程超时操作放入另外的工作线程中。在工作线程中可以通过handler和主线程交互。
3、提前或延迟操作,错开时间段提高TPS
(1) 延迟操作
不在Activity、Service、BroadcastReceiver的生命周期等对响应时间敏感函数中执行耗时操作,可适当delay。
Java中延迟操作可使用ScheduledExecutorService,不推荐使用Timer.schedule;
Android中除了支持ScheduledExecutorService之外,还有一些delay操作,如
handler.postDelayed,handler.postAtTime,handler.sendMessageDelayed,View.postDelayed,AlarmManager定时等。
(2) 提前操作
对于第一次调用较耗时操作,可统一放到初始化中,将耗时提前。如得到壁纸wallpaperManager.getDrawable();
4、网络优化
以下是网络优化中一些客户端和服务器端需要尽量遵守的准则:
a. 图片必须缓存,最好根据机型做图片做图片适配
b. 所有http请求必须添加httptimeout
c. api接口数据以json格式返回,而不是xml或html
d. 根据http头信息中的Cache-Control域确定是否缓存请求结果。
e. 减少网络请求次数,服务器端适当做请求合并。
f. 减少重定向次数
g. api接口服务器端响应时间不超过100ms
google正在做将移动端网页速度降至1秒的项目,关注中https://developers.google.com/speed/docs/insights/mobile
Android 性能优化方法
对于一些Android项目,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了 优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:
- private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
- VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
//设置最小heap内存为6MB大小。当然对于内存吃紧来说还可以通过手动干涉GC去处理
bitmap 设置图片尺寸,避免 内存溢出 OutOfMemoryError的优化方法
★android 中用bitmap 时很容易内存溢出,报如下错误:Java.lang.OutOfMemoryError : bitmap size exceeds VM budget
● 主要是加上这段:
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = 2;
● eg1:(通过Uri取图片)
- private ImageView preview;
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
- Bitmap bitmap = BitmapFactory.decodeStream(cr
- .openInputStream(uri), null, options);
- preview.setImageBitmap(bitmap);
以上代码可以优化内存溢出,但它只是改变图片大小,并不能彻底解决内存溢出。
● eg2:(通过路径去图片)
- private ImageView preview;
- private String fileName= "/sdcard/DCIM/Camera/2010-05-14 16.01.44.jpg";
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inSampleSize = 2;//图片宽高都为原来的二分之一,即图片为原来的四分之一
- Bitmap b = BitmapFactory.decodeFile(fileName, options);
- preview.setImageBitmap(b);
- filePath.setText(fileName);
★Android 还有一些性能优化的方法:
● 首先内存方面,可以参考 Android堆内存也可自己定义大小 和 优化Dalvik虚拟机的堆内存分配
● 基础类型上,因为Java没有实际的指针,在敏感运算方面还是要借助NDK来完成。这点比较有意思的是Google 推出NDK可能是帮助游戏开发人员,比如OpenGL ES的支持有明显的改观,本地代码操作图形界面是很必要的。
● 图形对象优化,这里要说的是Android上的Bitmap对象销毁,可以借助recycle()方法显示让GC回收一个Bitmap对象,通常对一个不用的Bitmap可以使用下面的方式,如
- if(bitmapObject.isRecycled()==false) //如果没有回收
- bitmapObject.recycle();
● 目前系统对动画支持比较弱智对于常规应用的补间过渡效果可以,但是对于游戏而言一般的美工可能习惯了GIF方式的统一处理,目前Android系统仅能预览GIF的第一帧,可以借助J2ME中通过线程和自己写解析器的方式来读取GIF89格式的资源。
● 对于大多数Android手机没有过多的物理按键可能我们需要想象下了做好手势识别 GestureDetector 和重力感应来实现操控。通常我们还要考虑误操作问题的降噪处理。
Android堆内存也可自己定义大小
对于一些大型Android项目或游戏来说在算法处理上没有问题外,影响性能瓶颈的主要是Android自己内存管理机制问题,目前手机厂商对RAM都比较吝啬,对于软件的流畅性来说RAM对性能的影响十分敏感,除了上次Android开发网提到的优化Dalvik虚拟机的堆内存分配外,我们还可以强制定义自己软件的对内存大小,我们使用Dalvik提供的 dalvik.system.VMRuntime类来设置最小堆内存为例:
- private final static int CWJ_HEAP_SIZE = 6* 1024* 1024 ;
- VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
//设置最小heap内存为6MB大小。当然对于内存吃紧来说还可以通过手动干涉GC去处理,我们将在下次提到具体应用。
优化Dalvik虚拟机的堆内存分配
对于Android平台来说,其托管层使用的Dalvik JavaVM从目前的表现来看还有很多地方可以优化处理,比如我们在开发一些大型游戏或耗资源的应用中可能考虑手动干涉GC处理,使用 dalvik.system.VMRuntime类提供的setTargetHeapUtilization方法可以增强程序堆内存的处理效率。当然具体原理我们可以参考开源工程,这里我们仅说下使用方法: private final static floatTARGET_HEAP_UTILIZATION = 0.75f; 在程序onCreate时就可以调用 VMRuntime.getRuntime().setTargetHeapUtilization(TARGET_HEAP_UTILIZATION); 即可。
【编辑推荐】
1.采用硬件加速,在androidmanifest.xml中application添加 android:hardwareAccelerated="true"。不过这个需要在android 3.0才可以使用。
2. View 中设置缓存属性. setDrawingCache为true.
3. 优化你的布局。通过Android sdk中tools目录下的layoutopt 命令查看你的布局是否需要优化。
4. 动态加载View. 采用ViewStub 避免一些不经常的视图长期握住引用.
5.将Acitivity 中的Window 的背景图设置为空。getWindow().setBackgroundDrawable(null); android的默认背景是不是为空。
6. 采用<merge> 优化布局层数。 采用<include >来共享布局。
7. 查看Heap 的大小
8. 利用TraceView查看跟踪函数调用。有的放矢的优化。
9. cursor 的使用。不过要注意管理好cursor,不要每次打开关闭cursor.因为打开关闭Cursor非常耗时。Cursor.require用于刷新cursor.
10.采用环形Buffer(可以采用链表数据结构实现)。可以设置一个链表长度的上限,根据手势的变化来不断地更新环形Buffer的内容。
11.采用SurfaceView在子线程刷新UI, 避免手势的处理和绘制在同一UI线程(普通View都这样做)。
12.采用JNI,将耗时间的处理放到c/c++层来处理。
13.有些能用文件操作的,尽量采用文件操作,文件操作的速度比数据库的操作要快10倍左右。
14. 懒加载和缓存机制。访问网络的耗时操作启动一个新线程来做,而不要再UI线程来做。
我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
.static关键字
当类的成员变量声明成static后,它是属于类的而不是属于对象的,如果我们将很大的资源对象(Bitmap,context等)声明成static,那么这些资源不会随着对象的回收而回收,
会一直存在,所以在使用static关键字定义成员变量的时候要慎重。
使用保守的Service
如果你的应用需要使用 service 在后台执行业务功能, 除非是一直在进行活动的工作(比如每隔几秒向服务器端请求数据之类)否则不要让它一直保持在后台运行. 并且, 当你的 service 执行完成但是停止失败时要小心 service 导致的内存泄露问题.
当你启动 service 时, 系统总是优先保持服务的运行. 这会导致内存应用效率非常低, 因为被该服务使用的内存不能做其它事情. 也会减少系统一直保持的LRU缓存处理数目, 使不同的app切换效率降低. 当前所有 service 的运行会导致内存不够不能维持正常系统的运行时, 系统会发生卡顿的现象严重时能导致系统不断重启.
最好的方式是使用 IntentSevice 控制 service 的生命周期, 当使用 intent 开始任务后, 该 service 执行完所有的工作时会自动停止.
在android应用中当不需要使用常驻 service 执行业务功能而去使用一个常驻 service 是最糟糕的内存管理方式之一. 所以不要贪婪的使用 service 使你的应用一直运行状态. 这样不仅使你因为内存的限制提高了应用运行的风险, 也会导致用户发现这些异常行为后而卸载应用.
当视图变为隐藏状态后释放内存
当用户跳转到不同的应用并且你的视图不再显示时, 你应该释放应用视图所占的资源. 这时释放所占用的资源能显著的提高系统的缓存处理容量, 并且对用户的体验质量有直接的影响.
当实现当前 Activity 类的 onTrimMemory() 回调方法后, 用户离开视图时会得到通知. 使用该方法可以监听 TRIM_MEMORY_UI_HIDDEN 级别, 当你的视图元素从父视图中处于隐藏状态时释放视图所占用的资源.
注意只有当你应用的所有视图元素变为隐藏状态时你的应用才能收到 onTrimMemory() 回调方法的 TRIM_MEMORY_UI_HIDDEN . 这个和 onStop() 回调方法不同, 该方法只有当 Activity 的实例变为隐藏状态, 或者有用户移动到应用中的另外的 activity 才会引发. 所以说你虽然实现了 onStop() 去释放 activity 的资源例如网络连接或者未注册的广播接收者, 但是应该直到你收到 onTrimMemory(TRIM_MEMORY_UI_HIDDEN)才去释放视图资源否则不应该释放视图所占用的资源. 这里可以确定的是如果用户通过后退键从另外的 activity 进入到你的应用中, 视图资源会一直处于可用的状态可以用来快速的恢复 activity.
内存资源紧张时释放内存
在应用生命周期的任何阶段 onTrimMemory() 回调方法都可以告诉你设备的内存越来越低的情况, 你可以根据该方法推送的内存紧张级别来释放资源.
TRIM_MEMORY_RUNNING_CRITICAL
应用处于运行状态并且不会被杀掉, 设备使用的内存比较低, 系统级会杀掉一些其它的缓存应用.
TRIM_MEMORY_RUNNING_LOW
应用处于运行状态并且不会被杀掉, 设备可以使用的内存非常低, 可以把不用的资源释放一些提高性能(会直接影响程序的性能)
TRIM_MEMORY_RUNNING_CRITICAL
应用处于运行状态但是系统已经把大多数缓存应用杀掉了, 你必须释放掉不是非常关键的资源, 如果系统不能回收足够的运行内存, 系统会清除所有缓存应用并且会把正在活动的应用杀掉.
还有, 当你的应用被系统正缓存时, 通过 onTrimMemory() 回调方法可以收到以下几个内存级别:
TRIM_MEMORY_BACKGROUND
系统处于低内存的运行状态中并且你的应用处于缓存应用列表的初级阶段. 虽然你的应用不会处于被杀的高风险中, 但是系统已经开始清除缓存列表中的其它应用, 所以你必须释放资源使你的应用继续存留在列表中以便用户再次回到你的应用时能快速恢复进行使用.
TRIM_MEMORY_MODERATE
系统处于低内存的运行状态中并且你的应用处于缓存应用列表的中级阶段. 如果系运行内存收到限制, 你的应用有被杀掉的风险.
TRIM_MEMORY_COMPLETE
系统处于低内存的运行状态中如果系统现在没有内存回收你的应用将会第一个被杀掉. 你必须释放掉所有非关键的资源从而恢复应用的状态.
因为 onTrimMemory() 是在级别14的android api中加入的, 所以低版本的要使用 onLowMemory() 方法, 该方法大致相当于 TRIM_MEMORY_COMPLETE 事件.
注意: 当系统开始清除缓存应用列表中的应用时, 虽然系统的主要工作机制是自下而上, 但是也会通过杀掉消费大内存的应用从而使系统获得更多的内存, 所以在缓存应用列表中消耗更少的内存将会有更大的机会留存下来以便用户再次使用时进行快速恢复.
检查可以使用多大的内存
前面提到, 不同的android设备系统拥有的运行内存各自都不同, 从而不同的应用堆内存的限制大小也不一样. 你可以通过调用 ActivityManager 中的 getMemoryClass() 函数可以通过以兆为单位获取当前应用可用的内存大小, 如果你想获取超过最大限度的内存则会发生 OutOfMemoryError .
有一个特别的情况, 可以在 manifest 文件中的 <application> 标签中设置 largeHeap 属性的值为 "true"时, 当前应用就可以获取到系统分配的最大堆内存. 如果你设置了该值, 可以通过ActivityManager 的 getLargeMemoryClass() 函数获取最大的堆内存.
然后, 只有一小部分应用需要消耗大量堆内存(比如大照片编辑应用). 从来不需要使用大量内存仅仅是因为你已经消耗了大量的内存并且必须快速修复它, 你必须使用它是因为你恰好知道所有的内存已经被分配完了而你必须要保留当前应用不会被清除掉. 甚至当你的应用需要消耗大量内存时, 你应该尽可能的避免这种需求. 使用大量内存后, 当你切换不同的应用或者执行其它类似的操作时, 因为长时间的内存回收会导致系统的性能下降从而渐渐的会损害整个系统的用户体验.
另外, 大内存不是所有的设备都相同. 当跑在有运行内存限制的设备上时, 大内存和正常的堆内存是一样的. 所以如果你需要大内存, 你就要调用 getMemoryClass() 函数查看正常的堆内存的大小并且尽可能使内存使用情况维护在正常堆内存之下.
避免在 bitmaps 中浪费内存
当你加载 bitmap 时, 需要根据分辨率来保持它的内存时最大为当前设备的分辨率, 如果下载下来的原图为高分辨率则要拉伸它. 要小心bitmap的分辨率增加后所占用的内存也要进行相应的增加, 因为它是根据x和y的大小来增加内存占用的.
注意: 在 Android 2.3.x(api level 10)以下, 无论图片的分辨率多大 bitmap 对象在内存中始终显示相同大小, 实际的像素数据被存储在底层 native 的内存中(c++内存). 因为内存分析工具无法跟踪 native 的内存状态所有调试 bitmap 内存分配变得非常困难. 然而, 从 Android 3.0(api level 11)开始, bitmap 对象的内存数据开始在应用程序所在Dalvik虚拟机堆内存中进行分配, 提高了回收机率和调试的可能性. 如果你在老版本中发现 bitmap 对象占用的内存大小始终一样时, 切换设备到系统3.0或以上来进行调试.
使用优化后的数据容器
利用 Android 框架优化后的数据容器, 比如 SparseArray, SparseBooleanArray 和 LongSparseArray. 传统的 HashMap 在内存上的实现十分的低效因为它需要为 map 中每一项在内存中建立映射关系. 另外, SparseArray类非常高效因为它避免系统中需要自动封箱(autobox)的key和有些值.
知道内存的开销
在你设计应用各个阶段都要很谨慎的考虑所使用的语言和库带来的内存上的成本和开销. 通常情况下, 表面上看起来无害的会带来巨大的开销, 下面在例子说明:
当枚举(enum)成为静态常量时超过正常两倍以上的内存开销, 在 android 中你需要严格避免使用枚举
java 中的每个类(包含匿名内部类)大约使用500个字节
每个类实例在运行内存(RAM)中占用12到16个字节
在 hashmap 中放入单项数据时, 需要为额外的其它项分配内存, 总共占用32个字节
使用很多的不必要类和对象时, 增加了分析堆内存问题的复杂度.
当心抽象代码
通常来说, 使用简单的抽象是一种好的编程习惯, 因为一定程度上的抽象可以提供代码的伸缩性和可维护性. 然而抽象会带来非常显著的开销: 需要执行更多的代码, 需要更长时间和更多的运行内存把代码映射到内存中, 所以如果抽象没有带来显著的效果就尽量避免.
使用纳米 Protocol buffers 作为序列化数据
Protocol Buffers 是 Google 公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化. 但是它更小, 更快, 更简单. 如果你决定使用它作为你的数据, 你必须在你的客户端代码中一直使用纳米 protocol buffer, 因为正常的 protocol buffer 会产生极其冗余的代码, 在你的应用生会引起很多问题: 增加了使用的内存, 增加了apk文件的大小, 执行速度较慢以及会快速的把一些限定符号打入 dex 包中.
尽量避免使用依赖注入框架
使用像 Guice 和 RoboGuice 依赖注入框架会有很大的吸引力, 因为它使我们可以写一些更简单的代码和提供自适应的环境用来进行有用的测试和进行其它配置的更改. 然而这些框架通过注解的方式扫描你的代码来执行一系列的初始化, 但是这些也会把一些我们不需要的大量的代码映射到内存中. 被映射后的数据会被分配到干净的内存中, 放入到内存中后很长一段时间都不会使用, 这样造成了内存大量的浪费.
谨慎使用外部依赖库
许多的外部依赖库往往不是在移动环境下写出来的, 这样当在移动使用中使用这些库时就会非常低效. 所以当你决定使用一个外部库时, 你就要承担为优化为移动应用外部库带来的移植问题和维护负担. 在项目计划前期就要分析该类库的授权条件, 代码量, 内存的占用再来决定是否使用该库.
甚至据说专门设计用于 android 的库也有潜在的风险, 因为每个库做的事情都不一样. 例如, 一个库可能使用的是 nano protobuf 另外一个库使用的是 micro protobuf, 现在在你的应用中有两个不同 protobuf 的实现. 这将会有不同的日志, 分析, 图片加载框架, 缓存, 等所有你不可预知的事情的发生. Proguard 不会保存你的这些, 因为所有低级别的 api 依赖需要你依赖的库里所包含的特征. 当你使用从外部库继承的 activity 时尤其会成为一个问题(因为这往往产生大量的依赖). 库要使用反射(这是常见的因为你要花许多时间去调整ProGuard使它工作)等.
也要小心不要陷入使用几十个依赖库去实现一两个特性的陷阱; 不要引入大量不需要使用的代码. 一天结束时, 当你没有发现符合你要求的实现时, 最好的方式是创建一个属于自己的实现.
优化整体性能
除了上述情况外, 还可以优化CPU的性能和用户界面, 也会带动内存的优化
使用代码混淆去掉不需要的代码
代码混淆工具 ProGuard 通过去除没有用的代码和通过语义模糊来重命名类, 字段和方法来缩小, 优化和混淆你的代码. 使用它能使你的代码更简洁, 更少量的RAM映射页.
使用签名工具签名apk文件
如果构建apk后你没有做后续的任何处理(包括根据你的证书进行签名), 你必须运行 zipalign 工具为你的apk重新签名, 如果不这样做会导致你的应用使用更多的内存, 因为像资源这样的东西不会再从apk中进行映射(mmap).
注意:goole play store 不接受没有签名的apk
分析你的内存使用情况
使用adb shell dumpsys meminfo +包名 等工具来分析你的应用在各个生命周期的内存使用情况, 这个后续博文会有所体现.
使用多进程
一种更高级的技术能管理应用中的内存, 分离组件技术能把单进程内存划分为多进程内存. 该技术一定要谨慎的使用并且大多数的应用都不会跑多进程, 因为如果你操作不当反而会浪费更多的内存而不是减少内存. 它主要用于后台和前台能各自负责不同业务的应用程序
当你构建一个音乐播放器应用并且长时间从一个 service 中播放音乐时使用多进程处理对你的应用来说更恰当. 如果整个应用只有一个进程, 当前用户却在另外一个应用或服务中控制播放时, 却为了播放音乐而运行着许多不相关的用户界面会造成许多的内存浪费. 像这样的应用可以分隔为两个进程:一个进程负责 UI 工作, 另外一个则在后台服务中运行其它的工作.
在各个应用的 manifest 文件中为各个组件申明 android:process 属性就可以分隔为不同的进程.例如你可以指定你一运行的服务从主进程中分隔成一个新的进程来并取名为"background"(当然名字可以任意取).