• Thứ Ba, 15/06/2004 02:31 (GMT+7)

    Xây dựng ứng dụng tự điển bỏ túi trên điện thoại di động

    Hiện các thiết bị cầm tay có hỗ trợ Java đã trở nên phổ biến, và đây chính là 'đất dụng võ' để các nhà lập trình J2ME thi thố các ứng dụng khác nhau. Việc phát triển ứng dụng khá dễ dàng khi thực hiện trên PocketPC, Palm hay các loại điện thoại di động (ĐTDĐ) có bộ nhớ và dung lượng lưu trữ khá lớn và tốc độ xử lý tương đối nhanh như Nokia series 60 (6600, 7650, 3650), Sony-Ericsson P800, P900, Motorola V700...

    Tuy nhiên, vấn đề trở nên khó khăn hơn đối với các loại ĐTDĐ phổ biến trên thị trường như Nokia series 40 (6100, 6800, 7210, 7250,..) hay Siemens SL45, SL55, M55, Samsung X100, V200...  Các loại ĐTDĐ này mặc dù có hỗ trợ Java (MIDP 1.0, CLDC 1.0) nhưng dung lượng lưu trữ và bộ nhớ thực thi rất giới hạn (khoảng 100-300KB heap size). Một vấn đề khác là việc giới hạn kích thước tối đa của ứng dụng mà máy có thể tải về. Các ứng dụng Java trên ĐTDĐ (còn được gọi là MIDlet) được nhóm lại với nhau để tạo thành file JAR (Java archive) rồi được triển khai và cài đặt trên ĐTDĐ bằng cách dùng cáp nối với máy tính hoặc thông qua mạng không dây theo phương thức OTA (Over The Air), phổ biến ở Việt nam hiện nay là thông qua mạng GPRS. Thường giới hạn dung lượng của file JAR tải về dưới 64KB, một số máy i-mode của hãng NTT DoCoMo chỉ cho phép tải file JAR dưới 30KB, thậm chí có máy dưới 10KB. Điều này gây rất nhiều khó khăn cho người lập trình trong việc thiết kế dữ liệu và viết chương trình. Tham khảo giới hạn kích thước file JAR của một số ĐTDĐ ở bảng 1.

    Lựa chọn giải pháp cho ứng dụng tự điển

    Để tạo ra một ứng dụng tự điển bỏ túi chạy trên máy di động hỗ trợ Java, chúng ta có hai cách. Cách thứ nhất, đặt dữ liệu tự điển trên máy server, ứng dụng Java chạy trên máy di động sẽ kết nối đến server thông qua mạng không dây để gửi yêu cầu và nhận kết quả trả về. Mọi việc lưu trữ hay tìm kiếm đều được thực hiện trên máy server, còn ứng dụng trên máy di động chủ yếu dùng để hiển thị giao diện và kết quả. Tuy nhiên, theo cách thức này, máy của người dùng phải kết nối mạng và điều này rất bất tiện vì phải trả chi phí cho kết nối mạng với tốc độ khá chậm và không ổn định của mạng không dây. Cách thứ hai, dữ liệu cùng với toàn bộ chương trình được cài đặt và lưu trữ hoàn toàn trên máy di động, người dùng có thể sử dụng bất cứ lúc nào mà không cần phải kết nối mạng. Vấn đề đặt ra với cách thứ hai là việc thiết lập dữ liệu và xây dựng ứng dụng đáp ứng bộ nhớ và khả năng xử lý giới hạn của máy di động. Đây chính là nội dung tôi muốn giới thiệu trong bài viết này.

    Thiết lập dữ liệu cho máy tính

    Dữ liệu tự điển sẽ được đọc từ file text rồi sau đó lưu trữ vào bộ nhớ của máy, chúng ta có thể lưu trữ vào bộ nhớ tạm của máy bằng cách dùng bảng băm (Hashtable), hay có thể dùng Record Store để lưu vào bộ nhớ ổn định. Record Store là một class trong gói javax.microedition.rms của MIDP (Mobile Information Device Profile) dùng để lưu trữ và quản lý dữ liệu được lưu trên máy (thường được lưu trữ bộ nhớ cố định). Việc sử dụng Record Store có một thuận lợi là dữ liệu từ file text chỉ cần đọc một lần duy nhất rồi sau đó được lưu thẳng vào máy. Mọi việc tìm kiếm hay truy xuất sau này được thực hiện trực tiếp trên Record Store.  Tuy nhiên, dung lượng bộ nhớ dành riêng cho Record Store lại khá hạn chế, tối đa 20KB cho các loại máy thuộc Nokia series 40, và ngay cả trên Palm, dung lượng này chỉ đến mức 64KB. Với hạn chế này, ta khó có thể tạo ra một dữ liệu tự điển bỏ túi đúng nghĩa (khoảng 50-100KB).  Mặt khác, nếu ta sử dụng hết dung lượng bộ nhớ này thì sẽ không thể lưu trữ thêm các ứng dụng Java khác. Vì thế, giải pháp ở đây là dùng Hashtable để lưu lên bộ nhớ tạm của máy.

    Cấu trúc của một file text chứa dữ liệu tự điển như sau:

    <từ thứ 1>|<nghĩa của từ số một>

    <từ thứ 2>|<nghĩa của từ số hai>

    ...

    <từ thứ n>|<nghĩa của từ số n>

    Ví dụ:

    bear|gấu

    duck|vịt

    monkey|khỉ

    ...

    Ta có thể lưu toàn bộ dữ liệu của tự điển trong một file duy nhất, rồi sau đó nạp thẳng vào Hashtable. Tuy nhiên, cách này không khả thi vì thời gian để đọc toàn bộ dữ liệu từ file text rồi sau đó lưu trữ vào bộ nhớ khá lâu (tốc độ xử lý của các máy di động không được nhanh). Giải pháp đưa ra là chia nhỏ dữ liệu thành nhiều file text khác nhau để chỉ đọc và nạp dữ liệu của mỗi file vào bộ nhớ khi cần truy xuất đến dữ liệu của file đó (phương pháp 'load on request'). Để thực hiện được việc này, ta cần thêm một file làm chỉ mục (index) để trỏ đúng đến file cần đọc. Mỗi dòng trong index chứa một chuỗi các ký tự đầu tiên của những từ mà các file dữ liệu chứa.

    Ví dụ:

    abcd

    efghij

    klmno

    pqrs

    tuvw

    xyz

    Nghĩa là, file dữ liệu thứ nhất chứa các từ bắt đầu bằng chữ A,B,C và D, file dữ liệu thứ hai chứa các từ bắt đầu bằng chữ E, F,G,H,I và J...  Khi người sử dụng tìm kiếm từ monkey chẳng hạn, file dữ liệu thứ 3 sẽ được nạp vào bộ nhớ. Vì vậy, ta sử dụng một mảng (array) các Hashtable với số phần tử đúng bằng số file dữ liệu (sẽ là 6 phần tử như trong ví dụ trên).

    Tuy nhiên, khi thực hiện cách này vẫn gặp phải vấn đề ràng buộc về bộ nhớ. Khi dữ liệu được nạp vào bộ nhớ có kích thước lớn hơn dung lượng dành riêng cho mỗi ứng dụng, chương trình sẽ gặp lỗi bộ nhớ (Out Of Memory). Ta giải quyết bằng cách thực hiện phương pháp 'dọn rác' bộ nhớ (garbage collection). Số lượng tối đa Hashtable chứa dữ liệu trong cùng một thời điểm sẽ được giới hạn lại. Nghĩa là, trong ví dụ trên, thay vì sẽ có lúc 6 Hashtable đều có chứa dữ liệu, ta sẽ giới hạn lại chỉ có tối đa 3 Hashtable có chứa dữ liệu trong một thời điểm nào đó, và các Hastable còn lại rỗng.

    Hiện thực chương trình

    Trước tiên, ta cần viết hàm đọc file text vào bộ nhớ. Vì dữ liệu được lưu trữ theo từng dòng trong file text, mà các input stream trong J2ME  lại không hỗ trợ việc đọc dữ liệu theo từng dòng, nên ta phải viết một class mới để thực hiện điều này, class này được đặt tên là LineTextLoader.

    package sevenbit.util;

    import java.io.*;

    public class LineTextLoader {

        InputStream is;

        /* contructor */

        public LineTextLoader(String filename) {

           is = this.getClass().getResourceAsStream(filename);         

        }

         /* read line */

        public String readLine()

        {

            try {

                int ch = is.read();

               

                if (ch==-1) {

                    is.close();

                    return null;

                }

                else

                {

                    String ret = '';

                    while (ch != -1 && ch != 13) {

                        if (ch!=10) ret += (char)ch;                   

                        ch = is.read();

                    }

                   

                    return ret;

                }

            }

            catch (Exception e) {

                System.out.println('Error when reading file ' +e.getMessage());

            }

                return null;

        }   

    }

    Tiếp theo, ta tạo ra một class gọi là Translator. Đây là class chính của chương trình, cung cấp những hàm cần thiết của một bộ tự điển như dịch một từ sang nghĩa khác, lấy về danh sách các từ có ký tự bắt đầu xác định v.v. 

    package sevenbit.dictionary2;

    import java.util.*;

    import sevenbit.util.LineTextLoader;

    public class Translator {

        private String[] files;  //  array chứa tên các file dữ liệu

        private Hashtable[] hash; // array chứa các Hashtable tương ứng

        private String[] indexs;  // bảng index, chứa dữ liệu đọc từ index file 

        private int maxNumberOfHash;  //  số lượng tối đa Hashtable chứa dữ liệu

        private int currentIndex = 0; // index của Hashtable hiện tại

        public Translator(String[] dataFiles, String indexFile, int maxNumberOfHash) {       

            files = dataFiles;                       

            int size = dataFiles.length;        

            // khởi tạo bảng index

            indexs = new String[size];

            initIndex(indexFile);

            // khởi tạo giá trị ban đầu cho các Hashtable là null

            hash = new Hashtable[size];

            for (int i=0; i<size; i++)

                hash[i] = null;

                   

            this.maxNumberOfHash = (maxNumberOfHash>=size)?(size-1):maxNumberOfHash;       

        }

    ...

    Để khởi tạo bảng index, ta đọc nội dung của file index rồi lưu từng dòng vào mảng indexs như sau:

    private void initIndex(String filename)

    {

            LineTextLoader loader = new LineTextLoader(filename);

            String strLine = loader.readLine();

            int i = 0;

            while (strLine != null && ! strLine.equals('')) {

                indexs[i++] = strLine;

                strLine = loader.readLine();

            }

            loader = null;       

     }

    Bảng index này dùng để trỏ đúng đến Hashtable cần truy xuất, thông qua hàm getIndex.  Hàm này sẽ nhận vào một ký tự (thường là ký tự đầu tiên của từ cần tìm), rồi trả về index tương ứng của Hashtable chứa ký tự đó trong mảng Hashtable.

    private int getIndex(char c) {

            for (int i=0; i<indexs.length; i++)

                if (indexs[i].indexOf(c)>=0)

                    return i;

     

            // không tìm thấy

            return indexs.length-1;

    }

    Hàm getIndex được dùng trong getHashTable. Hàm này nhận vào một từ rồi sau đó trả về Hashtable có khả năng chứa từ đó.

    private Hashtable getHashTable(String keyword) {

            // tìm index của hashtable

            int index = getIndex(keyword.charAt(0));       

     

            if (hash[index]==null || hash[index].size()<=0) {

                gc(); // garbage collection

                initHash(index); // đọc dữ liệu từ file text vào hashtable

            }       

            currentIndex = index;

            return hash[index];

    }

    Trước tiên chương trình sẽ tìm index của Hashtable có thể chứa từ cần tìm, rồi sau đó kiểm tra xem Hashtable này đã được nạp dữ liệu chưa, nếu chưa, hàm 'dọn rác' (garbage collection) sẽ được gọi, rồi sau đó đọc dữ liệu từ file text và đưa vào Hashtable này. Hàm dọn rác được hiện thực như sau:

    private void gc()

     {

            int count = 0; // số lượng hashtable có chứa dữ liệu

            for (int i=0; i<hash.length; i++)

                if (hash[i] != null && hash[i].size()>0) count++;

           

            if (count >= maxNumberOfHash)  {

                hash[currentIndex] = null; // hashtable gần nhất được truy xuất sẽ bằng null

                System.gc(); // gọi hàm garbage collection của hệ thống

            }              

     }

    Trong hàm trên, ta thấy rằng mỗi khi số lượng Hastable có chứa dữ liệu vượt quá số lượng tối đa cho phép, Hashtable được truy xuất đến lần gần đây nhất sẽ được giải phóng khỏi bộ nhớ, rồi sau đó yêu cầu hệ thống 'dọn rác'.  Ta có thể chọn Hashtable khác để giải phóng chứ không nhất thiết phải là Hashtable được truy xuất gần nhất. Có thể là Hashtable có index lớn nhất hay nhỏ nhất, hoặc một Hashtable ngẫu nhiên nào đó.

    Việc nạp dữ liệu từ file text vào bộ nhớ cũng tương tự như việc đọc file index, có nghĩa là đọc từng hàng một như sau:

    private void initHash(int index)

    {

            Hashtable hashtable = new Hashtable();

            LineTextLoader loader = new LineTextLoader(files[index]);

     

            int pos;

            String strLeft, strRight;

            String strLine = loader.readLine();

     

            while (strLine != null)

            {

                if ((pos = strLine.indexOf('|'))>0) {

                    strLeft = strLine.substring(0,pos);

                    strRight = strLine.substring(pos+1,strLine.length());

     

                    if (!strLeft.equals('') && !strRight.equals(''))

                        hashtable.put(strLeft.toLowerCase(),strRight);

                }

                strLine = loader.readLine();

            }

           

            hash[index] = hashtable;

            loader = null;       

     }

    Sau khi mọi vấn đề liên quan đến việc đọc dữ liệu đã được hoàn tất, ta có thể thực hiện việc tìm kiếm nghĩa của một từ thông qua hàm translate đơn giản như sau:

    public String translate(String keyword) {

            Hashtable currentHash = getHashTable(keyword);

            return (String)currentHash.get(keyword);       

    }

    Class Translator còn cung cấp một hàm khác gọi là getList dùng để trả về danh sách tất cả các từ có ký tự bắt đầu cho trước. Hàm này sẽ được dùng trong trường hợp không tìm ra được từ cần tìm.

    public String[] getList(String keyword)

    {       

            Hashtable currentHash = getHashTable(keyword); // lấy về hashtable tương ứng

            Enumeration enum = currentHash.keys();               

           

            String firstLetter = String.valueOf(keyword.charAt(0));

            String currentWord;       

            Vector v = new Vector();

     

            while (enum.hasMoreElements()) {

                currentWord = (String)enum.nextElement();

                if (currentWord.startsWith(firstLetter))

                    v.addElement(currentWord);

            }

           String[] retString = new String[v.size()];

            v.copyInto(retString);

            quicksort(retString, 0, retString.length - 1);

     

            return retString;

     }

    Vì dữ liệu trong file text không đảm bảo là đã được sắp xếp trước đó, ta cần sắp xếp lại theo thứ tự bằng các hàm sort. Ở đây, chương trình dùng hàm quick sort. Đến đây, ta có thể bắt tay vào việc tạo ra giao diện của chương trình tự điển. Toàn bộ mã nguồn của chương trình Pocket Dictionary có thể tìm thấy tại địa chỉ: www.7bit.biz/pocketdict.phhp.

    Tối ưu Code chương trình và giảm kích thước file JAR

    Sau khi dịch chương trình thành file JAR (mà về thực chất là một file nén), chúng ta có thể thấy rằng các file dữ liệu đã được nén lại một cách đáng kể (dữ liệu 100KB cộng với class chương trình được nén lại còn khoảng 50KB). Tuy nhiên ta có thể giảm kích thước của file JAR này thêm một lần nữa bằng cách dùng công cụ obfuscator.  Một obfuscator  thường bao gồm những đặc tính sau:

    1.     Loại bỏ những class không dùng đến

    2.     Loại bỏ những hàm và biến không dùng tới

    3.     Đổi tên class, package, hàm và biến

    4.     Thêm vào file class một số mã để chương trình khó bị dịch ngược (decompile)

    Đặc tính 1, 2 và 3 dùng để giảm kích thước file class của bạn (có thể giảm đến 30% ngay cả sau khi đã nén trong file JAR). Trong khi đó, đặc tính 3 và 4 dùng để bảo vệ chương trình của bạn chống lại các chương trình dịch ngược từ file class sang file mã nguồn (decompiler). Sau khi dùng obfuscator, chương trình sẽ khó bị dịch ngược hơn và ngay cả khi bị dịch ngược, chương trình sẽ khó 'đọc' hơn vì tên của class, package, hàm và biến đã bị thay đổi. Ba obfuscator miễn phí được dùng phổ biến nhất hiện nay là JAX (http://www.alphaworks.ibm.com/tech/JAX/), Retroguard (http://www.retrologic.com/retroguard-main.html) và Jshrink (http://www.e-t.com/jshrink.html).

    Các chương trình giả lập để kiểm tra chương trình

    Việc kiểm tra ứng dụng trên các chương trình giả lập rất cần thiết trước khi bạn triển khai ứng dụng trên các thiết bị thật. Bảng 2 cung cấp địa chỉ trên mạng cho tải về một số chương trình giả lập các loại ĐTDĐ phổ biến. Sau khi chạy thử trên các máy giả lập này, ta nên chạy kiểm tra ứng dụng trên các thiết bị thật nếu có điều kiện, vì không phải lúc nào chương trình cũng chạy tốt trên thực tế.

    Triển khai chương trình lên Internet

    Sau khi tạo file JAR cho chương trình, ta có thể đưa chương trình lên Internet để người sử dụng có thể tải trực tiếp về máy di động thông qua mạng không dây bằng phương thức OTA. Trước tiên, server mà ta đưa chương trình lên phải được cấu hình để hiểu được kiểu file JAR và JAD (Java Application Descriptor, file này được tạo khi ta tạo MIDlet). Server sẽ nhận dạng những file này bằng cách đặt giá trị cho header Content-Type của file JAD thành text/vnd.sun.j2me.app-descriptor và file JAR thành application/java-archive. (Bảng 3)

    Sau đó, trong file JAD, ta chỉnh lại giá trị của MIDlet-Jar-URL thành địa chỉ URL chứa file JAR. Ví dụ: MIDlet-Jar-URL: http://www.7bit.biz/wap/pocketdict.jar.

    Kế tiếp, ta tạo ra file WML chứa đường dẫn đến file JAD như sau:

    <?xml version='1.0' ?>

    <!DOCTYPE wml PUBLIC '-//WAPFORUM//DTD WML 1.1//EN' 'http://www.wapforum.org/DTD/wml_1.1.xml' >

    <wml>

    <head>

    <meta forua='true' http-equiv='Cache-Control'

    content='must-revalidate, no-store'/>

    </head>

    <template>

    <do type='prev' label='Back'>

     <prev/>

    </do>

    </template>

    <card id='main' title='Pocket Dictionary Download' newcontext='true'>

    <p align='center'>

    <a href='http://www.7bit.biz/wap/pocketdict.jad'>Pocket Dictionary</a><br/>

    </p>

    </card>

    </wml>

     

    Ta có thể thay file WML bằng file HTML. Tuy nhiên, lưu ý rằng một số máy di động không đọc được định dạng của file HTML.

    Sau khi upload file JAD, JAR cùng với file WML (hoặc HTML) lên server, người sử dụng có thể tải chương trình về bằng cách dùng máy di động truy cập vào địa chỉ URL của file WML (hoặc HTML).

    Nếu bạn muốn tải chương trình trực tiếp vào máy di động, hãy truy xuất vào địa chỉ www.7bit.biz/wap/wap.wml.

    Hướng phát triển

    Như vậy ta đã hoàn tất tất cả các giai đoạn tạo ra ứng dụng tự điển bỏ túi chạy trên ĐTDĐ.

    Chương trình này có thể được dùng để tạo ra các loại tự điển khác nhau như tự điển Anh-Anh hay Anh-Việt. Một khó khăn khác đặt ra là hiển thị tiếng Việt unicode trên các loại máy di động khác nhau.

    Ta cũng có thể cải tiến lại chương trình thành một loại tự điển hai chiều (tự điển song ngữ) trên cùng một dữ liệu. Điều này khá dễ dàng vì dữ liệu được lưu trữ ở dạng <từ>|<từ>, nên thay vì tìm kiếm theo từ bên trái thì ta làm ngược lại, tìm kiếm theo từ bên phải. 

    Không chỉ dừng ở ứng dụng tự điển, ta có thể phát triển chương trình thành một dạng sách tra cứu bỏ túi, chẳng hạn như chương trình Who Was Who (www.7bit.biz/whowaswho.php), là một ứng dụng tra cứu những người nổi tiếng trên thế giới.

    Mọi chi tiết, thắc mắc và đóng góp cho chương trình, xin liên hệ tác giả.ÿ

    Lê Phong
    apache@7bit.biz

    ID: A0405_106