Android Performance Tips

Khi thực hiện coding 1 ứng dụng trên 1 nền tảng bất kỳ nào đó thì việc tối ưu hiệu suất luôn luôn là vấn đề được đặt lên hàng đầu. Bài viết này chủ yếu bao gồm những thủ thuật code nhỏ để có thể cải thiện hiệu suất tổng thể của project. Tất nhiên nó không đảm bảo rằng những thay đổi này sẽ nâng cao perfomance 1 cách đáng kể của hệ thống. Việc chọn thuật toán hay thiết kế cấu trúc dữ liệu vẫn là ưu tiên hàng đầu trong mỗi project tuy nhiên nó nằm ngoài phạm vi của bài viết này. Bạn có thể tham khảo bài viết này đề hình thành nên những thói quen trong quá trình coding để tối ưu hoá từng dòng code nhỏ nhất.

Có hai nguyên tắc cơ bản cho việc viết mã hiệu quả:

Đừng làm công việc mà bạn không cần phải làm. Không phân bổ bộ nhớ nếu bạn có thể tránh nó. Một trong nhưng khó khăn nhất khi bạn thực hiện micro-optimizing là ứng dụng của bạn phải chạy trên được nhiều loại phần cứng. Các phiên bản khác nhau của VM chạy trên bộ vi xử lý khác nhau đang chạy ở tốc độ khác nhau. Nó nhiều khi không đơn giản chỉ là :”thiết bị X có phần cứng tốt hơn thiết bị Y”. Và bạn có thể quy đổi hiệu suất từ thiết bị này sang thiết bị khác theo tỉ lệ chênh lệch phần cứng. Cũng có 1 sự khác biệt lớn giữa thiết bị có và không có Just-In-Time (JIT) . Một đoạn mã tốt nhất cho thiết bị có JIT không có nghĩa là nó cũng tốt nhất cho thiết bị không có JIT. Vì vây vấn đề khi xây dưng ứng dụng là đảm bảo ứng dụng của bạn hoạt động tốt trên nhiều loại thiết bị, đảm bảo mã của bạn là hiệu quả ở tất cả các cấp và tối ưu hóa hiệu suất của bạn.

Avoid Creating Unnecessary Objects

Một đối tượng được tạo ra không bao giờ là “free”. Việc sử dụng Garbage collection quản lý và dọn dẹp các đối tượng “rác” có thể giúp giảm thiểu ít bộ nhớ hơn, tuy nhiên thực tế là khi đã phân bổ bộ nhớ bao giờ cũng tốn tài nguyên hơn là không phân bổ bộ nhớ 😄.

Khi bạn đã phân bổ bộ nhớ cho 1 object, bạn bắt buộc phải thu gom bộ nhớ rác định kỳ. Điều này cũng sẽ gây nên những tác động đến vấn đề trải nghiệm người dùng. Garbage collection (gc) bắt đầu xuất hiện từ Android 2.3 giúp cho việc gom “rác”. Tuy nhiên theo 1 lẽ tự nhiên là những việc không cần thiết thì lúc nào cũng nên tránh . Một ví dụ nhỏ: Khi bạn xây dựng 1 method trả về 1 string mà bạn biết rằng nó sẽ được 1 stringbuffer nào đó, bạn có thể sửa code sao cho nó được append trực tiếp vào buffer thay vì tao ra 1 temporary string trước khi append string. Một ví dụ khác triệt để hơn là việc bóc tách các mảng đa chiều thành các mảng 1 chiều song song. Một mảng kiểu int là tốt hơn nhiều so với một mảng của các đối tượng Integer, nhưng điều này cũng khái quát thực tế là hai mảng song song của ints cũng có rất nhiều hiệu quả hơn nhiều so với một mảng của (int, int) . Cũng vậy với bất kỳ mảng nguyên thuỷ nào khác. Nếu bạn cần phải thực hiện một container lưu trữ các paranel của (Foo, Bar) Object, cố gắng nhớ rằng hai mảng song song Foo [] và Bar [] tốt hơn nhiều so với một mảng duy nhất của các tùy chỉnh (Foo, Bar). Tât nhiên ở đây cũng có những ngoại lệ, ví dụ ở đây bạn cần 1 thiết kế các API cho các đoạn code khác truy cập vào. Trong những trường hợp đó, cũng cần có 1 sự “thoả hiệp” nhỏ ở đây để đạt được 1 cấu trúc API tốt nhất. Tuy nhiên trong các đoạn code nội bộ thì bạn nên cố gắng để nâng cao hiệu quả. Nói chung là tránh tạo ra nhưng temporary object không cần thiết nếu có thể. Điều đó sẽ giảm thiểu được việc phải đi gom rác thường xuyên, có thể ảnh hưởng đến perfomance.

Prefer Static Over Virtual

Nếu bạn không cần truy xuất vào các biến của object. Có thể làm cho phương thức của bạn thành static. Lúc đó việc gọi hàm sẽ nhanh hơn khoảng 15-20%. Việc này cũng có một lợi thế nữa là method của bạn khi được gọi cũng không làm ảnh hưởng đến object.

Use Static Final For Constants

Khi cần khai báo các biến dạng hằng số, Hãy cùng xem các khai báo:

static int intVal = 42;
static String strVal = "Hello, world!";

trình biên dịch sẽ tạo ra 1 class initializer method, gọi là <clinit>, được thực hiện khi class được sử dụng lần đầu. Method thực hiện gán giá trị 42 cho biện intVal và thực hiện kết nối từ bảng classfile string constant cho strVal. Khi các giá trị được tham chiếu về sau, chúng được truy cập qua field lookups để tìm kiếm. Chúng ta có thể cải thiện bằng cách thêm từ khoá final.

static final int intVal = 42;
static final String strVal = "Hello, world!";

Băng cách này class sẽ không đòi hỏi cinit method bởi vì các constant sẽ được init trong dex file. initVal sẽ sử dụng trực tiếp giá trị 42 và strVal sẽ sử dụng "string constant" để tham chiếu giá trị thay vì sử dụng file lookup

Avoid Internal Getters/Setters

Trong native code như C++ việc sử dụng getter (i = getCount ()) thay vì truy cập trực tiếp (i = mCount). Đây là 1 thói quen tốt và thường được áp dụng cho các ngôn ngữ hướng đối tượng khác như Java, C#. Bởi vì trình biên dịch có thể truy cập trực tiếp vào biến. Nếu bạn cần hạn chế hoạc debug, bạn có thể thêm code bất cứ lúc nào. Tuy nhiên ở trên Android thì nó là 1 ý tường tồi. Gọi 1 Virtual method tốn effort hơn rất nhiều so với field lookups. Nó là hợp lý cho việc làm quen với lập trình hướng đối tượng khi bạn sử dụng getter và setter trong các public interface. Tuy nhiên nội tại bên trong class thì bạn luôn luôn nên truy xuất trực tiếp vào biến.

Use Enhanced For Loop Syntax

Khi duyệt 1 mảng, có rất nhiều cách để chạy vòng lặp, cùng xem 1 số cách phổ biến:

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;
    }
}

zero() là châmj nhất, bời vì JIT không có thể chưa tối ưu hoá cost của việc lấy ra chiều dài của mảng cho mỗi lần thực hiện lệnh của vòng lặp.

one() nhanh hơn zero() vì nó đã kéo mọi thứ ra biến local và bỏ qua việc lookups . Chiều dài của mảng chỉ được kéo ra 1 lần.

two() là nhanh nhất với các device không có JIT, và ngang với 1 trong các thiết bị có JIT. Nó sử dụng các cú pháp tăng cường cho các vòng lặp được giới thiệu trong phiên bản 1.5 của ngôn ngữ lập trình Java.

Consider Package Instead of Private Access with Private Inner Classes

Xem xét đoạn code sau:

public class Foo {
    private class Inner {
        void stuff() {
            Foo.this.doStuff(Foo.this.mValue);
        }
    }

    private int mValue;

    public void run() {
        Inner in = new Inner();
        mValue = 27;
        in.stuff();
    }

    private void doStuff(int value) {
        System.out.println("Value is " + value);
    }
}

Điều quan trọng ở đây là chúng ta define 1 private inner class được accesses đến private method và private instance field của clas cha. Điều này tuy không sai, và giá trị in ra vẫn là 27. Tuy nhiên vấn đề là VM xem xét truy cập trực tiếp đến các private member của Foo từ Foo$Inner , điều này là không hợp lệ bởi vì Foo và Foo$Inner là các class khác nhau, mặc dù Java chấp nhận môt Inner class accss đến các private member của Outer class. Để thực hiện, trình biên dịch sẽ tạo ra một số method tổng hợp:

/*package*/ static int Foo.access$100(Foo foo) {
    return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
    foo.doStuff(value);
}

Code ở Inner class sẽ gọi đến static method cấn truy cập vào biến mValue hoặc gọi doStuff ở Outer class. Điều đó có nghĩa là bạn truy cập vào các member qua các accessor method. Mặc dù ở bên trên đã khuyên cáo việc access qua method là chậm hơn access trực tiếp qua biến. Nếu bạn đang sử dụng code như trên, bạn có thể thay đổi bằng cách định nghĩa lại các biến và method trong Inner class sang 1 class khác cùng package. Không may mắn là nếu như vậy, bạn không thể truy cập trực tiếp các biến và method trong các class cùng package. Vì vậy bạn không thể sử dụng chúng trong public API.

Avoid Using Floating-Point

Như một quy tắc, floating-point chậm hơn 2 lần so với integer trong các thiết bị android. Không có nhiều sự khác biện giữa float và duoble. Với các thiết bị để bàn khi tài nguyên lớn hơn. bạn có thể thay đổi double và float.

Ngoài ra còn rất nhiều phương pháp nhỏ khác để tối ưu hoá code của bạn trong từng dòng lệnh mà trên cơ sở 1 bài viết không thể trình bày hết được. Bạn có thể thêm khảo thêm tại Android Developer.