Performance tip trong Android
Lần trước chúng ta đã tìm hiểu sơ qua về Performance và cách quản lý bộ nhớ trong Android. Ở bài lần này chúng ta sẽ đến với các vi tối ưu mà có thể cải thiện performance của toàn bộ app khi kết hợp. Chọn đúng thuật toán và cấu truc dữ liệu luôn là ưu tiên trước hết, nhưng sẽ không được đề cập ở ...
Lần trước chúng ta đã tìm hiểu sơ qua về Performance và cách quản lý bộ nhớ trong Android. Ở bài lần này chúng ta sẽ đến với các vi tối ưu mà có thể cải thiện performance của toàn bộ app khi kết hợp. Chọn đúng thuật toán và cấu truc dữ liệu luôn là ưu tiên trước hết, nhưng sẽ không được đề cập ở bài này.
Có 2 nguyên tắc cơ bản trong việc viết code hiệu quả:
-
Đừng làm những việc không cần thiết
-
Không cấp phát bộ nhợ nếu bạn có thể phòng tránh nó
Một vấn đề khó khăn nhất mà bạn sẽ phải đối mặt với vi tối ưu hóa code là app của bạn chắc chắn sẽ được chạy trên nhiều dạng máy khác nhau. Những version khác nhay của VM chạy trên các bộ vi sử lý khác nhau với tốc độ khác nhau. Cụ thể, tính toán trên giả lập cho bạn biết rất ít về hiệu suất trên mọi thiết bị. Ngoài ra còn có sự khác biệt rất lớn giữa các thiết bị và không có JIT: code tốt nhất cho một thiết bị với một JIT không phải lúc nào cũng tốt nhất cho một thiết bị mà không có.
-
Tránh tạo các Object không cần thiết
Việc tạo object không bao giờ là miễn phí. Khi bạn phân bổ nhiều đối tượng trong ứng dụng của bạn, bạn sẽ buộc bộ thu gom rác thải định kỳ, tạo chút "trục trặc" với trải nghiệm người dùng. Việc thu gom rác đồng thời giới thiệu trong Android 2.3 sẽ giúp, nhưng công việc không cần thiết luôn luôn phải tránh.
Vì vậy, bạn nên tránh tạo ra những trường hợp đối tượng bạn không cần. Một số ví dụ về những điều có thể giúp:
-
Nếu bạn có một method trả về một chuỗi, và bạn biết rằng kết quả của nó sẽ luôn luôn được nối thêm vào một StringBuffer, thay đổi chữ ký và implement để các chức năng hiện phụ thêm trực tiếp, thay vì tạo ra một đối tượng tạm thời.
-
Khi giải nén chuỗi từ một tập hợp các dữ liệu đầu vào, cố gắng để trở về một chuỗi con của dữ liệu ban đầu, thay vì tạo ra một bản sao. Bạn sẽ tạo ra một đối tượng String mới, nhưng nó sẽ chia sẻ các char [] với dữ liệu.
Một ý tưởng có phần triệt để hơn là cắt nhỏ mảng đa chiều thành các đơn mảng một 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á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 kiểu int cũng có hiệu quả hơn nhiều so với một mảng của (int, int) đối tượng. Cũng vậy với bất kỳ sự kết hợp của các kiểu dữ liệu nguyên thủy.
-
Nếu bạn cần phải thực hiện một container lưu trữ các tuple của (Foo, Bar) đối tượng, cố gắng nhớ rằng hai song song Foo [] và Bar [] là mảng chung thì 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) đối tượng. (Các trường hợp ngoại lệ này, tất nhiên, là khi bạn đang thiết kế một API cho các mã khác để truy cập. Trong những trường hợp này, thường thì tốt hơn nên làm một sự thỏa hiệp nhỏ với tốc độ để đạt được một thiết kế API tốt. Nhưng trong mã nội bộ riêng, bạn nên cố gắng đạt hiệu quả tốt nhất.)
Nói chung, tránh tạo các đối tượng tạm thời, ngắn hạn, nếu bạn có thể. đối tượng ít được tạo ra có nghĩa là ít thường xuyên thu gom rác thải, trong đó có một tác động trực tiếp đến trải nghiệm người dùng.
-
-
Nên tạo Static hơn Virtual
Nếu bạn không cần phải truy cập vào các trường của một đối tượng, tạo cho method của bạn tĩnh. Lời gọi sẽ nhanh hơn khoảng 15% -20%.
-
Sử dụng Static Final cho các biến cố định
Hãy xem xét việc khai báo sau ở đầu của một class:
static int intVal = 42; static String strVal = "Hello, world!";
Trình biên dịch tạo ra một lớp phương thức cơ bản, gọi là <clinit>, được thực hiện khi các lớp được sử dụng lần đầu. Phương pháp lưu trữ các giá trị 42 vào intval, và trích ra một reference từ các bảng string classfile cố định cho strVal. Khi các giá trị được tham chiếu về sau, chúng được truy cập với các filed lookup. Chúng ta có thể cải thiện vấn đề với từ khóa "final":
static final int intVal = 42; static final String strVal = "Hello, world!";
Các class không còn đòi hỏi một method <clinit>, bởi vì các hằng số đi vào khởi tạo static trong file dex. Code đó đề cập đến intval sẽ sử dụng các giá trị số nguyên 42 trực tiếp, và truy cập đến strVal sẽ sử dụng một tương đối rẻ tiền "chuỗi liên tục" chỉ dẫn thay vì một field lookup.
-
Tránh các getter/setter nội bộ
Trong các ngôn ngữ native như C ++ đó là thực tế phổ biến để sử dụng getter (i = getCount ()) thay vì truy cập trường trưc tiếp (i = mCount). Đây là một thói quen tuyệt vời cho C ++ và thường được thực hiện bằng các ngôn ngữ hướng đối tượng khác như C # và Java, bởi vì trình biên dịch thường có thể truy cập inline, và nếu bạn cần phải hạn chế hoặc truy cập trường gỡ lỗi bạn có thể thêm code bất cứ lúc nào.
Tuy nhiên, đây là một ý tưởng tồi trên Android. Virtual method đắt hơn nhiều nhiều so với sinh trường lookup. Đó là hợp lý theo thực hành lập trình hướng đối tượng phổ biến và có getter, setters trong giao diện public, nhưng trong một class mà bạn nên luôn luôn truy cập vào các trường trực tiếp.
Nếu không có một JIT, truy cập trường trực tiếp là về 3x nhanh hơn so với cách gọi một getter tầm thường. Với JIT (nơi truy cập field trực tiếp rẻ như truy cập vào một local), truy cập trực tiếp field là khoảng 7x nhanh hơn so với cách gọi một getter bình thường.
-
Sử dụng Enhanced với vòng lặp
Việc enhanced cho các vòng lặp (cũng đôi khi được gọi là "for-each" loop) có thể được sử dụng cho các collection mà thực hiện các giao diện Iterable và cho mảng. Với collection, một iterator được phân bổ để thực hiện cuộc gọi giao diện để hasNext() và next(). Với một ArrayList, một vòng lặp được tính là nhanh hơn 3x (có hoặc không có JIT), nhưng đối với các collection khác, tăng cường cho các cú pháp vòng lặp sẽ được chính xác tương đương với việc sử dụng lặp rõ ràng.
Có một số lựa chọn thay thế cho lặp qua một mảng:
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ậm nhất, vì JIT có thể chưa tối ưu hóa đi các chi phí của việc chiều dài mảng một lần cho mỗi lần lặp của vòng lặp. one() là nhanh hơn. Nó kéo tất cả mọi thứ ra thành các biến địa phương, tránh việc tra cứu. Chỉ có chiều dài mảng cung cấp một lợi ích hiệu suất. two() là nhanh nhất cho các thiết bị mà không có JIT, và không thể phân biệt từ one() cho 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 các ngôn ngữ lập trình Java. Vì vậy, bạn nên sử dụng enhanced cho các vòng lặp theo mặc định, nhưng hãy xem xét một vòng lặp được tính toán hiệu suất cho ArrayList iterator.
-
Tránh sử dụng Floating-Point
Floating-point là chậm hơn khoảng 2 lần so với số nguyên trên các thiết bị Android. Xét về tốc độ, không có sự khác biệt giữa float và double trên các phần cứng hiện đại. Double lớn hơn gấp 2 lần. Như với máy tính để bàn, giả sử không gian không phải là một vấn đề, bạn nên chọn double hơn là float.
Ngoài ra, ngay cả đối với số nguyên, một số bộ vi xử lý có nhân cứng nhưng thiếu phần cứng divide. Trong trường hợp như vậy, các hoạt động phân chia số nguyên và mô đun được thực hiện trong phần mềm mỗi khi bạn đang thiết kế một bảng băm hoặc làm rất nhiều về toán học.
-
Luôn luôn tính toán
Trước khi bạn bắt đầu tối ưu hóa, chắc chắn rằng bạn có một vấn đề mà bạn cần phải giải quyết. Hãy chắc chắn rằng bạn có thể đo chính xác hiệu suất hiện tại của bạn, hoặc bạn sẽ không thể để đo lường lợi ích của các lựa chọn thay thế.