优化Android上的Java代码【笔记】

前言:代码优化并不是应用开发的首要任务,而提供良好的用户体验并专注于代码的可维护性才是首要的任务。

1.Android如何执行代码


Android平台将Java代码编译成Java字节码,并通过dex编译器将其编译成Dalvik字节码,最终交由Dalvik虚拟机(JVM是基于栈的虚拟机,而Dalvik是基于寄存器的虚拟机)执行。在Android 2.2之后,引入了Dalvik JIT(实时编译器),它把Dalvik字节码编译成本地代码,由于本地代码直接由CPU执行,而绕过了虚拟机,并且可以将本地代码为特定架构进行优化,所以可以明显加快执行速度。(在manifest.xml文件里可以用Android:vmSafeMode启用或禁用JIT编译器。默认是启用的)


2.将递归转为迭代(以斐波那契数列为例)


3.数据类型的修正


在Java中,long是64位、int是32位、short是16位。所有整数类型都是有符号的。如斐波那契数列第92项是超出Long型范围的,那么我们怎么办?Java中有个类——java.math.BigInteger。该对象可以可以容纳任意大小的有符号整数,该类定义了所有基本的数学运算。(Java.math包除了BigInteger还定义了BigDecimal参考,而java.lang.Math提供了数学常数和运算函数。如果应用不需要双精度,使用Android的FloatMath效果更佳)


4.使用BigInteger带来的问题


它有三大缺点:


(1)BigInteger是不可变的,例如a=a.add(b),而不能简单的写成a.add(b),所以每次都会创建一个新的BigInteger对象;


(2)BigInteger使用BigInt和本地代码实现,每分配一个BigInteger对象都会额外创建一个BigInt对象,从Java调用JNI调用本地代码会产生一定的开销;


(3)数字越大,相加运算花费的时间也越长。


5.通过优化算法来减少分配内存数量


尽管我们需要BigInteger来确保结果的正确性,但也不必使用BigInteger来计算所有的n值。既然基本类型long可以容纳小于等于92项的结果,可以混合BigInteger和基本类型。


通过优化算法的同时,还可以使用BigInteger实现的预分配对象,比如BigInteger.ZERO、BigInteger.ONE、BigInteger.TEN。


6.通过类的static代码来预先计算结果


但带来的缺点是可能会造成较大的内存使用


7.缓存先前的计算结果


如果计算的代价过高,最好把过去的结果缓存起来,下次就可以很快的取出来。比如Java中的HashMap就可以充当缓存,不过,Android定义了SparseArray类,当键是整数时,它比HashMap效率更高。因为HashMap使用的是java.lang.Integer对象,而SparseArray使用的是基本类型int。因此使用HashMap会创建很多的Integer对象。但是换句话说,尽管使用HashMap回避SparseArray慢一些,但这样的好处是可以让代码不依赖与Android平台。(Android定义了多种类型的稀疏数组:SparseArray[键为整数,值为对象]、SparseBooleanArray[键为整数,值为boolean]、SparseIntArray[键为整数,值为整数])

android.util.LruCache<K,V>,该类是Android3.1引入的,还可以自己通过继承java.util.LinkedHashMap,并覆写removeEldestEntry来实现类似的功能。

8.注意API的等级


试图调用不存在的API将导致应用崩溃,你通常可以使用Build.VERSION.SDK_INT获得Android平台的API等级。可是不幸的是,这个字段是在Android1.6(API等级为4)中引入的,所以如果你试图在早于其的版本中使用该字段,则会导致崩溃。另一种选择是使用Build.VERSION.SDK,它是API等级1引入的,但这一字段现在已经废弃,版本字符串也没有归档。

可以用反射来检查是否存在SDK_INT字段,即判断该平台是不是Android1.6或跟高版本,参见Class.forName("Android.os.Build$VERSION").getField("SDK"),反射技术也可以用来确认平台是否存在特定方法,参见Class.forName("XXX")和Class.getMethod(“XXX”)。不过使用反射会使代码变慢,因此在性能至关重要的地方应尽量避免使用反射。替代的方法是在静态初始化代码中调用以上方法以确认指定方法是否存在,在性能要求较高的地方只调用Method.invoke()就好了。

9.数据结构


Java.util包中常见的数据结构,在此基础之上,Android为了解决性能问题还增加了一些自身的实现:

LruCache\SparseArray\sparseBooleanArray\SparseIntArray\Pair

Java还定义了Array类和Collections类。这两个类只包含静态方法,分别操作数组和集合。例如,使用Array.sort对数组排序,使用Arrays.binarySearch在有序数组中搜索值。

每当需要选择一个数据结构来解决问题时,最好将选择范围缩小到只有几个类,因为通常每个类为特定目的或为特定的服务而优化。例如,如果你不需要在数据结构内部处理同步,应该选择ArrayList而不是Vector。如果你使用基于散列的数据结构(例如HashMap),而且键是自定义的对象,那么你需要确保正确地覆写了类定义中的equal和hashCode方法。hashCode的低劣实现可以轻易地将散列的收益化为乌有。可以参考这个 链接,里面有实现hashCode()的很好示例。

10.响应能力


应用的性能不仅仅在于速度,也要能让用户真正感觉到快才行。例如,显得更快的方法有,应用可以延迟创建对象,知道需要时才创建,成为推迟初始化的技术。另外,在开发过程中,你很有可能要在关注性能的地方侦测执行缓慢的代码。

@优化的基本原则是保持应用的持续响应,可以考虑使用多线程。

@优化Activity的启动或者销毁序列是非常重要的:当一个Activity被销毁时,并创建一个新实例,会调用以下序列:onPause\onStop\onDestroy\onCreate\onStart\onResume。应用可以在manifest.xml文件中指定每个Activity元素的Android:configChanges属性,让它只接受自己想处理的配置变化。这会导致调用Activity的onConfigurationChanged(),而不是销毁。

@由于内存分配需要花时间,等到对象真正需要使用时才进行分配,也是一个很好的选择。当某个对象并不是立即就要使用时,推迟创建对象有着很明显的好处。

10.1降低布局复杂性,加速资源文件的展开


(1)使用RelativeLayout代替嵌套LinearLayouts,尽可能保持“扁平化”的布局。此外减少创建的对象数量,也会让事件的处理速度加快。

(2)使用ViewStub推迟对象创建,它可以在运行时展开资源,当ViewStub需要展现时,它被相应的资源展开替换,自己就成为了等待垃圾回收的对象。


10.2 使用StrictMode检测不良行为


例如遇到了下列两种情况:

(1)网络很慢(服务器很久都没有响应)

(2)文件系统的访问速度很慢

结论就是,不应该在主线程中进行网络操作或访问文件系统。通常情况下,在应用启动时,即当onCreate被调用时,启用StrictMode, 参考、 参考2

Android 3.0中引入了需要特别留意的方法有detectCustomSlowCall()和noteSlowCall(),他们都是用来检测应用中执行缓慢的代码或潜在缓慢的代码。

10.3 SQLite的性能提升


(1)如果SQL语句是简单的字符串,需要解释或编译才可以执行,当你执行execSQL语句时,SQLite内部是编译执行的。而事实证明,执行SQLite语句可能需要相当长的一段时间。除了编译,语句本身可能还需要创建。由于String是不可改变的,这可能会遇到如之前所说的创建了过多的BigInteger对象的问题。如果我们想创建一个完整的数据库,则需要大量的增删改的操作,而每个INSERT语句都会创建一个String对象并调用execSQL,在内部进行解析。一个显而易见的优化方法是,加快要执行的SQL语句字符串的创建速度。在这种情况下,使用+运算符来连接字符串不是最有效的方法,而使用StringBuilder对象,或调用String.format可以提高性能。【它们只是优化了传递给execSQL的字符串创建速度,这两种方法并不算是与SQL相关的优化】

如果所有的INSERT语句都十分相似,那么我们就可以考虑使用compileStatement让语句在循环外只编译一次。由于只进行一次的语句编译,并且绑定值是比编译更轻量的操作,所以这种方法明显快多了。Android还提供了其他的API,使用ContentValue对象把值插入到数据库中,它基本上包含了列名和值之间的绑定信息,并通过调用db.insert()方法来实现插入。该方法是最快实现且最灵活的选择。【Android3.0的android.database和android.database.sqlite包发生了许多变化。例如,Activity类中的managedQuery、startManagingCursor和stopManagingCursor方法已经废弃,由CursorLoader取而代之】

Android还提供了一些可以提高性能的类。例如,可以使用DatabaseUtils.InsertHelper在数据库中插入多行,这样就只需编译一次INSERT语句。

(2)虽然以上的例子中并没有显式地创建任何事务,但会自动为每个插入操作创建一个事务,并在每次插入后立即提交。显式创建事务有以下两个基本特性:

【1】原子提交【2】性能更好

抛开对性能的追求,第一个特性是很重要的。原子提交意味着数据库的所有修改要么全都完成,要么全都不做。事务不会只提交部分修改。最重要的一点是,有些数据库不是保存在内存中,而是存储在持久性存储上(比如SD卡或内部闪存),众所周知,访问持久性存储比访问易失性记忆体慢得多,所以数据库工作的大量时间都花费在此,一次性事务是解决此类问题的较好办法,事务处理的优越性能够得到更显著的体现。

(3)我们可以使用限制数据库访问的方式来加快查询的速度。数据库查询仅会返回一个游标对象,然后用它来遍历结果。很明显如果查询时选择正确的参数,只读取需要的数据将使性能得到可观的提升,如果将所有行为作为一个事务处理则会更快,如果只需要一定数量的行,指定调用查询的限制参数,可以进一步减少数据库的访问时间。【还可以考虑使用SQLite的FTS(全文检索)扩展,它支持更多高级搜索特性(使用索引)。 参考】


上一篇:如何使用ABAP代码反序列化JSON字符串成ABAP结构


下一篇:推荐 7 个提升前端编程效率的 VSCode 插件