Giảm các lớp không cần thiết :
Bước đầu tiên để giảm kích thước của chương trình đó là loại bỏ việc tạo những lớp không cần thiết. Chỉ nên tách lớp mới khi điều đó thực sự quan trọng và có ý nghĩa. Các lớp có nhiều xử lý, nhiều thông tin tương tự nhau thì nên kết hợp lại thành một lớp chung. Khi một lớp được tạo ra, trình biên dịch sẽ bổ sung thêm rất nhiều thông tin về lớp, định nghĩa thêm các hằng số, tạo bảng tra biến cục bộ
(LocalVariableTable), bổ sung hàm tạo mặc định… Chúng ta có thể thấy rõ điều này thông qua ví dụ về một lớp tối thiểu như sau :
public class DoNothing {
Tập tin DoThing.java sau khi được biên dịch thành tập tin DoNothing.class sẽ có kích thước là 257 bytes trong đó chỉ có 5 bytes là bytecode. Chúng ta có thể
xem thông tin về lớp này bằng cách thực hiện dòng lệnh :
javap –v DoNothing
Hạn chế sử dụng inner class và anonymous class :
Một trong những lớp nên loại bỏ đó là các lớp nội (inner classes) và các lớp
vô danh (anonymous classes). Trong ngôn ngữ Java, các anonymous classes thường
được dùng để cài đặt xử lý cho các sự kiện (event listener). Nếu thực sự cần thiết, chúng ta có thể khai báo một lớp chung để xử lý tất cả các sự kiện bởi vì nhiều đối tượng có thể sử dụng chung nhiều listener khác nhau. Lớp được tạo ra sẽ khai báo implements các interfaces cần thiết ví dụ như CommandListener, ItemStateListener
v.v…, nhờ vào tham số được truyền cho các phương thức commandAction và
itemStateChanged, chúng ta có thể phân biệt đối tượng nào cần được xử lý hành
động.
Các inner classes cũng làm tăng thêm kích thước của chương trình do
compiler phải tạo thêm các biến và các phương thức đặc biệt để giúp cho lớp này có thể truy xuất các thuộc tính và phương thức private của lớp đang chứa nó, và ngược lại, giúp cho các lớp đang chứa inner class có thể truy xuất các thành phần private của lớp này.
Giảm cây kế thừa :
Trong thiết kế cần hạn chế việc kế thừa nhiều cấp và không sử dụng lớp trừu tượng nếu không cần thiết.
Giảm chiều dài các định danh (identifiers) :
Nên đặt tên ngắn cho các hằng, biến, các phương thức và tên lớp. Ngoài ra, do mỗi ứng dụng J2ME đều được đóng gói riêng rẽ nên các lớp trong nhiều ứng dụng khác nhau trên thiết bị di động không thể xung đột nhau. Vì thế, nếu một lớp không phải xây dựng để trở thành một thành phần trong thư viện thì không cần thiết
phải đặt lớp này trong một package (không dùng từ khóa package ở đầu tập tin mã nguồn). Điều này có tác dụng giảm được một số lượng bytes trong lớp.
Hạn chế sử dụng getter / setter :
Khi cần cho phép các đối tượng của lớp khác được truy xuất một thuộc tính của lớp, nên khai báo thuộc tính này ở mức public thay vì sử dụng các hàm get, set. Thực ra, đây không phải là một phong cách lập trình tốt, tuy nhiên loại bỏ hàm get, set cũng giúp chúng ta giảm được kích thước của lớp.
Lưu ý khi khởi tạo mảng :
Nên tránh khởi tạo trực tiếp các phần tử của mảng. Thông thường, nếu chúng ta biết trước giá trị của từng phần tử trong mảng, chúng ta sẽ khai báo như ví dụ sau
int[] arr = {11, 22, 33, 44, 55, 66};
Khi viết dòng lệnh trên, chúng ta hi vọng rằng Java sẽ sao chép các giá trị được khởi gán vào vùng nhớ vừa được cấp phát cho mảng. Tuy nhiên, nếu chúng ta dịch ngược bytecode của đoạn chương trình này (cú pháp : javap –c tên_lớp) sẽ được kết quả như sau :
Kết quả trên chứng tỏ rằng Java không sao chép trực tiếp vào vùng nhớ của mảng mà thực hiện lần lượt các câu lệnh tương ứng như sau :
Do vậy, kích thước của tập lớp sau khi biên dịch sẽ tăng lên đáng kể vì phải chứa rất nhiều byte code được lặp đi lặp lại để gán giá trị cho từng phần tử
của mảng. 4: aload_0 5: bipush 6 7: newarray int 9: dup 10: iconst_0 11: bipush 11 13: iastore 14: dup 15: iconst_1 16: bipush 22 18: iastore 19: dup 20: iconst_2 21: bipush 33 23: iastore 24: dup 25: iconst_3 26: bipush 44 28: iastore 29: dup 30: iconst_4 31: bipush 55 33: iastore 34: dup 35: iconst_5 36: bipush 66 38: iastore arr[0] = 11; arr[1] = 22; arr[2] = 33; arr[3] = 44; arr[4] = 55; arr[5] = 66;
Tóm lại, khi kích thước của mảng lớn, chúng ta không nên khởi tạo trực tiếp giá trị ban đầu của các phần tử trong mảng, thay vào đó, lưu các dữ liệu này trên một tập tin tài nguyên và xây dựng hàm đọc rồi gán giá trị cho mảng khi thực thi.
Giảm kích thước các tập tin tài nguyên :
Nên cố gắng giảm kích thước các tập tin tài nguyên như các files dữ liệu, icons, các tập tin hình ảnh. Thông thường, chúng ta chỉ có thể hạn chế kích thước của hình ảnh, ít khi giảm được trên các tập tin dạng khác.
Hạn chế sử dụng các gói thư viện bổ sung :
Các gói thư viện của hãng thứ ba sẽ được đóng gói vào trong ứng dụng, bao gồm cả những lớp mà chúng ta không cần đến. Vì thế, không nên sử dụng nếu không thực sự cần thiết.
5.2.2. Tối ưu kích thước chương trình khi đóng gói :
• Phân tích quá trình đóng gói ứng dụng :
Hình 5-1 : Mô hình quá trình đóng gói ứng dụng J2ME
Các files mã nguồn được trình java compiler biên dịch ra dạng byte code. Mỗi lớp trong mã nguồn (bao gồm cả inner class) sẽ được biên dịch thành các tập tin .class riêng rẽ. Các tập tin này, cùng với các icons, các tập tin dữ liệu, hình ảnh v.v… được trình đóng gói kết hợp lại và nén chung trong một tập tin JAR (Java
S Soouurrcceeccooddee ( (..jjaavvaa)) ccooJJmmaappvviaialleerr B Byytteeccooddee ( (..ccllaassss)) Archive builder R Reessoouurrcceess . .JJAADD . .JJAARR
Archive) hay còn được gọi là một MIDlet Suite. Ngoài ra, trình đóng gói cũng tạo ra một tập tin mô tảứng dụng – JAD (Java Application Descriptor) nhằm cung cấp cho phần mềm quản lý ứng dụng trên thiết bị di động các thông tin cần thiết để
phầm mềm này xác định ứng dụng của chúng ta có thích hợp để thực thi trên thiết bị
hay không.
Như vậy, xét toàn bộ quá trình biên dịch và đóng gói ứng dụng, chúng ta có thể thấy rằng ngoài các biện pháp giúp giảm kích thước chương trình như đã trình bày ở phần trước, chúng ta vẫn còn có thêm cơ hội giảm kích thước của ứng dụng nếu (có thể) giảm được kích thước của các tập tin .class, có nghĩa là giảm số byte code trên tập tin .class sau khi biên dịch. Tuy nhiên, việc sửa đổi byte code là việc làm rất khó khăn và nguy hiểm. Dù vậy, chúng vẫn có thể giảm được một số lượng bytes nhất định bằng cách giảm đến mức tối đa số ký tự dùng để đặt tên cho các biến thành phần, các hằng số và các phương thức.
Phân tích tập tin .class :
Để xem danh sách các biến thành phần, các hằng số và phương thức trong lớp, chúng ta có thể sử dụng công cụjavap có trong bộ jdk. Cú pháp như sau :
javap –private tên_tập_tin_class
Xét ví dụ lớp Districts (tập tin Districts.class) trong chương trình. Sau khi
được decompiled, lớp Districts có dạng sau : // Imports
import javax.microedition.lcdui.Graphics; public final class Districts {
// Fields
private static int[] _$551; private static short[] _$552; private static String[] _$548; private static byte[] _$550; private static short[] _$555; private static short[] _$556;
private static short[] _$553; private static short[] _$554; private static short[] _$547; private static short[] _$549; // Constructors
public Districts() { } // Methods
public static final void destroy() { }
public static final void drawDistrictNames(Graphics g) { } public static final void drawDistricts(Graphics g) { } }
Nhận xét :
-Đối với các phương thức và thuộc tính được khai báo ở mức private, tên của chúng được trình biên dịch thay thế bằng một mã có dạng _$xxx.
-Đối với các phương thức và thuộc tính được khai báo ở mức public, tên của các thành phần này được giữ nguyên vì chúng có thểđược các đối tượng của những lớp khác truy xuất đến.
Ý tưởng thực hiện :
- Rút ngắn hơn nữa số ký tự dùng đểđặt tên cho các thành phần ở mức private. - Đặt lại tên cho các phương thức và thuộc tính được khai báo ở mức public sao cho tối ưu nhất (số ký tự ít nhất và không được trùng với các thành phần của lớp khác).
Sử dụng obfuscator :
Trên thị trường hiện nay có rất nhiều phần mềm hỗ trợ giúp chúng ta thực hiện việc tối ưu kích thước cho tập tin JAR (được gọi là obfuscator). Các phần mềm này đa số đều hoạt động dựa theo nguyên tắc giảm chiều dài các thuộc tính và phương thức của lớp nhưđã nêu ở phần trên. Các phần mềm công cụ này phần lớn là miễn phí. Tiêu biểu như proguard (http://proguard.sourceforge.net), retroguard (http://www.retrologic.com/retroguard-main.html)...
Hình 5-2 : Sơ đồ sử dụng Obfuscator
Ví dụ :
Xét tập tin District.class sau khi sử dụng proguard 3.2 (lớp Districts đã được obfuscator đổi tên thành g. S Soouurrcceeccooddee ( (..jjaavvaa)) ccooJJmmaavvppaiailleerr BB(y(y..tctceellaaccssooss)dd)ee Archive builder ....JJJJAAAADDRR R Reessoouurrcceess O Obbffuussccaattoorr . .JJAADD . .JJAARR
// Imports
import javax.microedition.lcdui.Graphics; public final class g {
// Fields
private static int[] a; private static short[] b; private static String[] c; private static byte[] d; private static short[] e; private static short[] f; private static short[] g; private static short[] h; private static short[] i; private static short[] j; // Constructors
public g() { } // Methods
public static final void a() { }
public static final void a(Graphics graphics) { } public static final void b(Graphics graphics) { } private static final void c(Graphics graphics) { } }
Kết quả sau khi sử dụng obfuscator vào chương trình :
Ghi chú : kích thước file JAR không tính các tập tin tài nguyên
Kích thước file JAR (bytes) Số bytes được rút ngắn (bytes) Tỉ lệ rút ngắn Không tối ưu kích thước 33.213 Sử dụng proguard 3.2 21.255 11.958 36,0 % Sử dụng retroguard 2.0.1 22.632 10.581 31,9 % Bảng 5-1 : Bảng so sánh kết quả sử dụng obfuscator 5.3. Tối ưu hóa về tốc độ :
5.3.1. Khái quát chung :
- Khi muốn tối ưu về tốc độ, trước tiên, cần tập trung vào tối ưu việc thiết kế
và lựa chọn giải thuật cho hợp lý, sau đó mới thực hiện việc cải thiện tốc độ ở mức cấp thấp trên từng dòng lệnh bởi vì phần lớn thời gian chương trình hoạt động được dùng cho việc thực hiện các hàm xử lý trong thư viện do chúng ta gọi đến, đặc biệt là đối với các chức năng xử lý đồ họa cấp thấp.
- Quy tắc 90/10 : “trong các chương trình có nhu cầu xử lý đồ họa lớn, đặc biệt là các phần mềm trò chơi hành động, 90% tổng thời gian xử lý của chương trình dùng để xử lý 10% mã nguồn”. Như vậy, 90% mã nguồn còn lại chỉ cần 10% thời gian để xử lý. Do đó, chúng ta cần phải tập trung xác định những thao tác xử lý nào tiêu tốn nhiều thời gian xử lý nhất để tìm cách tối ưu hóa chúng.
- Hiệu suất thi hành các lệnh không giống nhau trên thiết bị của các nhà sản xuất khác nhau do mỗi hãng sản xuất có công nghệ và cách cài đặt xử lý theo đặc tả
MIDP khác nhau.
5.3.2. Tìm các vị trí cần tối ưu về tốc độ :
Sử dụng Profiler
Trong bộ công cụ lập trình J2ME của Sun (Sun Wireless Toolkit) có cung
cấp cho chúng ta một tiện ích dùng để thống kê tần suất hoạt động và tài nguyên CPU dành cho từng chức năng xử lý (công cụ này được gọi là profiler). Dựa vào
bảng thống kê này, chúng ta có thể xác định được hàm nào được gọi thực hiện nhiều nhất, chi phí dành cho mỗi lần thực hiện là bao nhiêu và cho con số tổng cộng về
tổng số tài nguyên được cung cấp cho mỗi hàm trong toàn bộ thời gian mà chương trình hoạt động. Từ bảng thống kê này, chúng ta có thể xác định được hàm nào sử
dụng nhiều tài nguyên nhất để tập trung vào việc tối ưu nó.
Để hiển thị profiler, trong màn hình chính của Sun Wireless Toolkit, chọn
menu Edit Æ Preferences. Chọn tab Monitor, đánh dấu check vào ô Enable
Profiling trong khung Profiler. Sau khi trình giả lập kết thúc việc thực thi ứng dụng, cửa sổ Methods’ Profiler hiện ra như sau :
Hình 5-3 : Màn hình giao diện công cụ Profiler
Xác định thời gian thực thi :
Profiler giúp chúng ta xác định được số lượng chu kỳ xử lý cần cho mỗi hàm, tuy nhiên lại không giúp chúng ta xác định được thời gian cụ thể cho mỗi hàm xử lý và profiler cũng không được cung cấp trên thiết bị thật. Vì vậy, chúng ta có
thể xây dựng riêng một đoạn mã nhằm xác định khoảng thời gian dùng cho hàm bằng cách sử dụng giờ hệ thống :
long startTime = System.currentTimeMillis(); //Trước khi gọi hàm gọi_hàm();
long timeTaken = System.currentTimeMillis() – startTime; //Khoảng thời gian
thực thi
5.3.3. Các nguyên tắc tăng tốc cho chương trình :
- Chỉ nên tập trung tối ưu các thao tác có số lượng chu kỳ xử lý lớn.
- Hầu hết các hàm trong thư viện đồ họa cấp thấp ví dụ như các hàm vẽđối tượng đồ họa cơ sở (đường thẳng, hình chữ nhật, cung, ellipse…), các hàm tô màu
đều sử dụng rất nhiều chu kỳ máy, vì vậy, cần hạn chế số lần gọi các hàm này. - Sử dụng hàm setClip(…) để hạn chế vùng cần vẽ.
- Đem các tính toán ra bên ngoài vòng lặp nếu có thể.
- Nếu không làm ảnh hưởng đến thiết kế chương trình, cài đặt các phương thức dạng static final sẽ giúp phương thức được thực hiện nhanh hơn. Tránh sử
dụng phương thức synchronized, phương thức này chậm do phải lock những đối tượng cần thiết. Dạng phương thức Tốc độ synchronized chậm nhất interface chậm instance final nhanh static nhanh nhất
- Việc khởi tạo một đối tượng inner class mất gấp đôi thời gian so với việc khởi tạo một đối tượng của lớp thường.
- Đối với các hàm thường xuyên được sử dụng, cài đặt sao cho việc truyền tham số cho hàm càng ít càng tốt.
- Hạn chế việc tách một hàm lớn thành nhiều hàm nhỏ vì khi chia thành nhiều hàm nhỏ sẽ khiến cho lớp tăng kích thước, đồng thời sẽ phải mất thời gian cho việc truyền tham số giữa các hàm mặc dù cách làm này giúp chúng ta dễ dàng hơn trong công tác bảo trì.
- CPU của máy tính cũng như của các thiết bị di động thực hiện việc so sánh một số với số 0 nhanh hơn so với các số khác 0. Trong các vòng lặp nên tận dụng
điều này bằng các đếm ngược các biến đếm. Thông thường, chúng ta cài đặt vòng lặp như sau :
for (int i=0; i<n; i++) {
……. }
Nhưng, sẽ nhanh hơn nếu chúng ta cài đặt ngược lại :
for (int i=n; --i>=0; ) {
……. }
- Các phép toán trên bit có tốc độ rất cao, vì vậy, nên sử dụng các phép toán dịch bit (shift left, shift right) thay cho phép toán nhân, chia với các số là lũy thừa của 2.
- Trong ngôn ngữ Java, việc truy xuất một phần tử của mảng chậm hơn rất nhiều so với ngôn ngữ C. Mỗi khi chúng ta truy xuất một phần tử, Java đều kiểm tra chỉ sốđưa vào có hợp lệ hay không. Nếu chỉ số này nhỏ hơn 0 hoặc lớn hơn chỉ số
Như vậy, một phần tử trong mảng nếu được truy xuất nhiều lần thì chúng ta nên gán giá trị của phần tửđó vào một biến trung gian để sử dụng cho những lần tiếp theo.