Related Articles:
How to Keep Your App Running in the Background: Research on Android Process Protection and Alarm Background Termination
Android Enhancements: Essential Skills for Senior Engineers!
[Must-Read] 2017 is Here, The Most Comprehensive Interview Guide – These Android Interview Questions You Definitely Need
Source: Sunzxyong
http://blog.csdn.net/u010687392/article/details/50035061
Performance optimization is a vast topic, and I have always been very focused on learning in this area. Performance optimization includes many aspects, such as: I/O optimization, network operation optimization, memory optimization, data structure optimization, code structure optimization, UI rendering optimization, CPU resource usage optimization, exception handling optimization, and so on…
This article will discuss some areas of optimization in Android development based on my understanding.
ArrayList vs. Vector
Both ArrayList and Vector are lists implemented internally using arrays. The only difference between them is their support for multithreading: ArrayList is not thread-safe, while Vector is thread-safe because most of its methods are synchronized. Since Vector is thread-safe, its performance is certainly not as good as ArrayList (this assumption is correct), but it depends on the context. ArrayList is more efficient in operations like add, get, and remove, while Vector performs better in terms of memory. This is fundamentally due to the expansion strategy of ArrayList, which will be analyzed later.
Using fori to traverse collections that implement the RandomAccess interface.
First, let’s talk about the traversal methods for List collections. There are three: foreach, iterator, and fori.
In general development, foreach is usually the preferred choice for traversal due to its high efficiency. This viewpoint is correct, but it needs to be contextualized.
Below is my test of the traversal performance of an ArrayList containing 1 million entries using these three methods:
long start = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
data.get(i);
}
long end = System.currentTimeMillis();
Log.v(“zxy”,”fori time taken:”+(end-start));
start = System.currentTimeMillis();
for (Integer integer : data) {
}
end = System.currentTimeMillis();
Log.v(“zxy”,”foreach time taken:”+(end-start));
Iterator<Integer> iterator = data.iterator();
start = System.currentTimeMillis();
while (iterator.hasNext()){
iterator.next();
}
end = System.currentTimeMillis();
Log.v(“zxy”,”iterator time taken:”+(end-start));
11-19 09:11:44.276 1418-1418/? V/zxy: fori time taken:30
11-19 09:11:44.380 1418-1418/? V/zxy: foreach time taken:105
11-19 09:11:44.476 1418-1418/? V/zxy: iterator time taken:95
Typically, the high-efficiency foreach traversal method is not as effective as fori, as fori shows the best performance. This is because both ArrayList and Vector are implemented using arrays, allowing for quick random access. For lists that support random access, the JDK implements the RandomAccess interface, indicating support for fast random access.
When traversing a LinkedList with 10,000 entries:
11-19 09:33:23.984 1737-1737/? V/zxy: fori time taken:351
11-19 09:33:23.988 1737-1737/? V/zxy: foreach time taken:2
11-19 09:33:23.992 1737-1737/? V/zxy: iterator time taken:4
foreach performs best, so for arrays or lists implementing the RandomAccess interface, fori is the optimal traversal method, while for LinkedLists or collections implemented as linked lists, foreach or iterator is the best choice since foreach is implemented via iterator.
We can determine which traversal method to use for a List as follows:
if (list instanceof RandomAccess)
{
for (int i = 0; i < list.size(); i++) {}
} else {
Iterator<?> iterator = list.iterator();
while (iterator.hasNext()) {
iterator.next();
}
}
When constructing an ArrayList, specify the initial size whenever possible.
The internal expansion strategy of ArrayList is to increase the capacity by 1.5 times when the number of stored elements exceeds its current size. For example, if the current capacity of the ArrayList is 10,000 and it needs to store one more element (the 10,001st element), it will expand to 15,000, wasting memory resources. Additionally, expansion requires copying the entire array, and the default size of an ArrayList is 10. Therefore, setting the capacity of the ArrayList reasonably can avoid unnecessary expansion. The internal code for expansion and array copying in ArrayList is as follows:
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ?
MIN_CAPACITY_INCREMENT : s >> 1)];
System.arraycopy(a, 0, newArray, 0, s);
array = a = newArray;
In contrast, Vector’s internal expansion strategy is to expand by 1 each time:
if (capacityIncrement <= 0) {
if ((adding = elementData.length) == 0) {
adding = 1;
}
} else {
adding = capacityIncrement;
}
E[] newData = newElementArray(elementData.length + adding);
Similarly, among various Map collections, each has its own expansion strategy. For instance, HashMap doubles its original capacity when expanded. In commonly used String concatenation classes like StringBuffer and StringBuilder, there are also internal expansion strategies, typically expanding by 1.5 times.
Therefore, for APIs that require expansion, if the data size is known in advance, it is advisable to set the initial size ahead of time. This can not only avoid space waste due to expansion but also prevent the internal call to System.arraycopy() for large data copying.
If your program needs to access a List randomly via an index, prefer using ArrayList or Vector, and avoid using LinkedList unless absolutely necessary.
While ArrayList may not perform as well as Vector in terms of memory, it offers higher efficiency in data operations, especially in mobile devices like Android. Thus, it is acceptable to sacrifice a bit of space for time, while using Vector when thread safety is a concern.
If a method does not need to use the object’s members, declare it as static.
Calling a static method is 15% to 20% faster than calling an instance method because the method signature indicates that the method call will not affect the state of the object.
Using the final keyword wisely
The final keyword is commonly used for defining constants and methods, and many people often associate final with immutability. However, final also has a significant impact on performance optimization.
For example: static int AGE = 10; When 10 is referenced later, it requires a field lookup process, which for int types means looking up the integer constant pool in the method area. However, for final constants, this lookup is eliminated. For instance: static final int AGE = 10; In places where AGE is used, it will directly replace it with 10.
However, this optimization technique is only effective for basic types and String types; it does not apply to other reference types. Nonetheless, adding static final when declaring constants is still a good practice.
Additionally, the final keyword has a powerful effect when applied to frequently used methods that are already determined to be final. Why is this beneficial?
Before explaining this, let’s discuss the execution process of methods in Java. When a method is called, it is first pushed onto the stack. After execution, it is popped off the stack, and resources are released. Internally, this is a process of transferring memory addresses. When a method is called, it effectively transfers the execution address to the memory address where the method is stored. Before this operation, it is necessary to save the original program’s execution memory address. After the method execution is complete and popped off the stack, the program continues execution at the saved address. This is the method calling process.
Thus, the method calling process requires both space and time. Optimizing the frequent calls of the same method essentially involves inlining.
Inlining functions are optimizations done at compile time, where the compiler replaces the calls of marked inline functions directly with the entire function body, thus saving the time resources spent on function calls. However, this increases the target code size. Therefore, inlining is a strategy for trading space for time, which can be very beneficial for mobile devices.
To make a function an inline function, simply declare it as final. The compiler will automatically perform inlining optimization for final functions, so when that function is called, its body is directly expanded for use.
In summary, having more inline functions is not always better. On one hand, it does improve the running efficiency of the program, but on the other hand, excessive use of inline functions can backfire, potentially making a method’s body larger. Additionally, for methods with large bodies, the time taken for inlining may exceed the time taken for calling the method, which would not improve performance but rather lower it.
Overall, we can use final to modify frequently used methods that are already final and have small bodies to enhance program performance.
Prefer using built-in system code rather than writing your own.
The system provides many convenient APIs for us to use, such as System, Arrays, Collections, String, etc. These are much easier to use than writing them ourselves. Additionally, many Android APIs are implemented in lower-level C/C++, making them faster than our own implementations. Similarly, for system APIs, the DVM often uses inlining to enhance performance.
Use exceptions cautiously.
Using exceptions cautiously does not mean avoiding exceptions altogether but rather avoiding using exceptions to perform certain operations. For example, some people might use exceptions to forcefully interrupt certain operations. This is because throwing exceptions always executes the fillInStackTrace(); method, which readjusts the stack, making it essential to avoid unnecessary use of exceptions.
Feel free to add more insights.
Java and Android Experts Channel
We welcome you to follow us and discuss technology together. Scan and long press the QR code below to quickly follow us. Or search for the WeChat public account: JANiubility.
Public Account:JANiubility