Trao đổi với tôi

http://www.buidao.com

5/30/10

[Symbian] Chương trình hoạt động trên Symbian 2

Leave-Symbian exeption

1. Cơ chế bắt lỗi trên Symbian:
Nếu bạn đã quen với lập trình C++ hay Java thì exeption handling là một khái niệm chẳng xa lạ gì. Đây là cơ chế giúp ta quản lý các lỗi phát sinh. Lúc Symbian được thiết kế thì cơ chế exeption chưa được giới thiệu trong C++ hơn nữa sau này khi được giới thiệu thì nó cũng tỏ ra không phù hợp trong môi trường hạn chế về xử lý và bộ nhớ như Symbian bởi chúng làm tăng đáng kể kích thước mã biên dịch và tốn nhiều RAM, lại không thật sự hiệu quả.
Vì vậy Symbian đã đưa ra một cơ chế quản lý lỗi cho riêng mình được biết dưới tên gọi "leave". Do đó tuy Symbian sử dụng cú pháp C++ nhưng không hề có từ khóa try, catch hay throw đâu, các bạn nên chú ý điều này.
Trong một môi trường mà tài nguyên hạn hẹp như Symbian thì một cơ chế bắt lỗi hiệu quả và ít tốn kém sẽ rất cần thiết. "Leave" đã đáp ứng điều này. Cơ chế hoạt động của "leave" như sau: khi xảy ra một lỗi nào đó (thiếu bộ nhớ để cấp phát, thiếu vùng nhớ để ghi, lỗi trong truyền thông hay thiếu năng lượng cho các tài nguyên,...) thì hàm đang hoạt động sẽ bị ngắt lại, quyền điều khiển sẽ được chuyển đến phần chỉ thị sửa lỗi.
Xét về mặt cú pháp thì cơ chế "leave" này khá tương đồng với cơ chế của C++. Hàm đang thực thi bị ngắt bởi một cuộc gọi đến hàm User::Leave() hay User::LeaveIfError() khá giống với throw trong C++ còn 2 marco TRAP và TRAPD trên Symbian thì tương đồng với try và catch trên C++.
Ví dụ:
TInt result;
TRAP(result, MyLeaveL());
if (KErrNone==result)
{
//Code
}
User::LeaveIfError(result);

2. Hàm leave:
Như tôi đã nói trong phần quy ước trên Symbian, hàm có thể leave thì sẽ kết thúc bằng chữ L. Một hàm có thể leave nếu nó:
- Gọi hàm có thể leave mà không được gọi kèm với các trap harness như TRAP hay TRAPD.
- Gọi một trong các hàm hệ thống đảm nhận leave nhu User::Leave() hay User::LeaveIfError(),...
- Có dùng toán tử new(Eleave).
Chắc có lẽ có nhiều bạn sẽ thắc mắc tại sao tôi lại quá chú trọng đến "leave" như vậy. Thật ra "leave" là một khái niệm rất cơ bản trên Symbian bởi vì: thứ nhất, nguồn tài nguyên trên Symbian khá hạn hẹp nên lỗi thiếu tài nguyên hay xảy ra, thứ 2, nếu bạn không chú ý kỹ đến nó, nhất là phần thế nào là một hàm leave thì bạn sẽ gặp phải lỗi rất lớn trong lập trình trên Symbian: làm "lủng" bộ nhớ. Chi tiết về điều này mời các bạn xem bài sau: Cleanup stack.

Cleanup stack

Tại sao trong bài trước tôi lại nói nếu bạn không chú ý đến quy ước cũng như xem xét hàm của bạn có phải là hàm leave hay không thì có thể code bạn viết gây "lủng" bộ nhớ, đó là vì: nếu hàm của bạn có leave xảy ra thì tại thời điểm leave, điều khiển sẽ được chuyển đến phần xử lý lỗi, lúc này vùng stack cho hàm có leave này sẽ được giải phóng, các biến khai báo cục bộ trong hàm này sẽ bị xóa đi. Đối với các biến khai báo kiểu T trên stack thì không sao nhưng đối với các biến kiểu C khai báo trên heap hay các biến kiểu R thì đây là vấn đề nghiêm trọng. Bởi lẽ theo đúng quy trình thực thi, nếu không có gì xảy ra thì vào cuối hàm, chúng ta sẽ hủy vùng nhớ đối tượng trên heap qua toán tử delete hay gọi hàm Close() cho các biến kiểu R nhưng nếu giữa chừng hàm bị ngắt trước khi ta gọi các hàm hủy này thì rõ ràng các đối tượng này sẽ không được giải phóng hoàn toàn, tạo ra lỗ hổng trên bộ nhớ.
Ví dụ:
void UnsafeFunctionL()
{
CExClass* test = CExClass::NewL(); //Hàm có thể leave
test->FunctionMayLeaveL();
delete test;
}
Điều gì xảy ra khi hàm FunctionMayLeaveL() bị leave, lúc này hàm UnsafeFunctionL() sẽ bị ngắt, stack sẽ bị xóa, biến test bị bị xóa nhưng vùng nhớ cấp cho nó trên heap qua hàm CExClass::NewL() thì vẫn còn và lúc này không ai quản lý nó cả, nó bị "mồ côi" trên heap. Vùng nhớ cấp phát này sẽ tồn tại mà không được giải phóng tại ra một lỗ hổng trong bộ nhớ.
Vậy bây giờ ta phải làm sao đây để luôn đảm bảo không bị lỗ hổng trên bộ nhớ khi có leave xảy ra? Symbian đã đưa ra khái niệm mới là Cleanup Stack.
Cleanup stack là một ngăn xếp có nhiệm vụ giải phóng các vùng nhớ cấp cho các đối tượng được đưa vào chúng trước đó khi leave xảy ra.
Ví dụ: dùng hàm trên với cleanup stack:
void SafeFunctionL()
{
CExClass* test = CExClass::NewL(); //Hàm có thể leave
CleanupStack:: push(test);
test->FunctionMayLeaveL();
CleanupStack:: pop(test);
delete test;
}
Lúc này nếu có leave xảy ra thì chúng ta vẫn không sợ bị lủng bộ nhớ vì cleanup stack đã giải phóng vùng nhớ cho biến test giùm chúng ta rồi nhờ hàm: CleanupStack:: push(test).
- Một lưu ý là nếu leave không xảy ra thì chúng ta phải lấy đối tượng cần hủy ra khỏi cleanup stack qua hàm CleanupStack:: pop(test) (chúng ta có thể dùng nhiều cách pop khác nhau, chi tiết các bạn xem qua lớp CleanupStack).
- Một lưu ý khác là với những hàm kế thúc bằng LC, nghĩa là có thể leave và đã có push lên cleanup stack rồi, nên sau khi gọi hàm này, bạn phải gọi hàm CleanupStack:: pop() hoặc CleanupStack:: popAndDestroy() nếu không sẽ bị lỗi. Lỗi này tôi cũng đã nói trong phần quy ước rồi, các bạn chú ý nhé, hay bị lỗi này lắm đó.
- Đối với các đối tượng kế thừa các lớp khác ngoài lớp C thì khi hủy cleanup stack chỉ có thể hủy vùng nhớ mà không thể gọi destructor như đối với lớp C được nên Sym bian đề xuất một số hàm push khác cho phù hợp: CleanupReleasePushL() để chỉ giải phóng vùng nhớ (đối tượng lớp T), CleanupDeletePushL() để chỉ thực thi destructor (đối tượng lớp M) hay CleanupClosrPushL() để giải phóng tài nguyên cấp cho các đối tượng lớp R.
Leave và Cleanup stack là một cặp bài trùng tạo nên sự an toàn cho lập trình trên Symbian. Đây là 2 khái niệm rất cơ bản, nếu không hiểu vè nó, trong khi lập trình có thể bạn sẽ gặp lỗi mà không biết đường sửa hay có thể gặp vài lỗi rất ngớ ngẫn.


Two-phase construction

Đến đây chắc bạn đã thấy là Symbian hỗ trợ quản lý bộ nhớ tốt như thế nào, đảm bảo cả trong điều kiện lỗi vẫn không bị lủng bộ nhớ. Lý do khiến Symbian rất chú trọng đến việc này là so với PC, điện thoại di động có bộ nhớ không lớn bằng hơn nữa các ứng dụng trên điện thoại đôi khi phải chạy hàng tháng, thậm chí hàng năm (nếu ta không tắt máy).
Từ bài cleanup stack, có người thắc mắc là có phải tất cả các lớp đều có hàm NewL() và NewLC() để khởi tạo đối tượng không và nhận thấy mình thiếu một phần quan trọng đi liền sau leave và cleanup stack là khởi tạo 2 pha (two-phase construction) nên hôm nay tôi sẽ nói về nó luôn, tạm gác phần descriptor lại.

1 Two-phase construction:
Theo đúng cú pháp C++, một đối tượng mới sẽ được cài đặt như sau:
Code:

CExam* exam = new (Eleave) CExam();

Hệ thống sẽ làm gì với code trên: đầu tiên, một vùng nhớ sẽ được cấp trên heap cho đối tượng lớp CExam nhớ hàm new, rồi sau đó constructor của lớp CExam sẽ được gọi để hoàn tất việc khởi tạo đối tượng. Điều gì sẽ xảy ra nếu hàm constructor của lớp này bị leave, rõ ràng là bộ nhớ sẽ bị lủng do heap đã cấp một vùng cho đối tượng foo rồi và như vậy vùng nhớ này sẽ bị "mồ côi" trên heap. Lúc này ắt hẳn có bạn sẽ nghĩ ngay đến cleanup stack, nếu được như vậy thì tôi rất mừng vì bạn đã biết vai trò của cleanup stack ở chỗ nào, thế nhưng tiếc thay trong trường hợp này lại không dùng được. Tai sao ư? Tại vì để làm được điều đó thì hàm CleanupStack:: push phải đặt trong lòng toán tử new, điều này có thể sao?!
Vì vậy Symbian đưa ra một luật là: constructor không được phép leave. Nhưng đôi khi trong phần khởi tạo của chúng ta lại có phần có thể leave thì sao, chẳng hạn như cấp phát bộ nhớ hay tạo một session truy cập tài nguyên. Trong tình huống đó, khởi tạo 2 pha (two-phase construction) sẽ giúp bạn:
- Pha 1: Phần constructor đơn giản, không leave. Phần này sẽ được gọi liền ngay sau khi toán tử new được gọi.
- Pha 2: Một hàm khác sẽ đảm nhận việc hoàn tất khởi tạo, trên Symbian thường đặt tên là ConstructL(), hàm này có thể leave.
Code:

CExam* exam = new (Eleave) CExam();//pha 1 CleanupStack:: push (exam); exam->ContructL();//Pha 2 CleanupStack:: pop();

Và bây giờ thì ta đã yên tâm là bất cứ có gì xảy ra, bộ nhớ vẫn nguyên vẹn.

2. NewL() và NewLC():
Nhưng cách trên bất tiện ở chỗ là khi khởi tạo 1 đối tượng lại phải gọi 2 pha với 4 hàm, vừa bất tiện vừa dễ quên. Do đó Symbian tiếp tục đưa ra một khái niệm nữa để giúp cho lập trình viên chúng ta tránh được sự hay quên và dài dòng này.
Trong lớp CExam, chúng ta tạo thêm 2 hàm tĩnh (có từ khóa static đằng trước phần khai báo thông thường) NewL() và NewLC() như sau:
Code:

CExam* CExam::NewLC() { CExam* me = new (Eleave) CExam();//pha 1 CleanupStack:: push (me); exam->ContructL();//Pha 2 return me; } CExam* CExam::NewL() { CExam* me = CExam::NewLC(); CleanupStack:: pop (me); return me; }

Và lúc này việc khai báo đối tượng của bạn sẽ vừa dễ dàng lại đảm bảo:
Code:

CExam* exam = CExam::NewL();

Lưu ý: Ở trên các bạn thấy tôi luôn xài Eleave sau toán tử new. Nhờ nó mà nếu không cấp phát được, leave sẽ xảy ra. Nếu không có nó rõ ràng sau hàm new bạn phải kiểm tra xem có cấp phát thành công không, rất mất công lại làm code phức tạp thêm.
Code:

CExam* exam = new (Eleave) CExam(); if (NULL != exam) { //cấp phát thành công }

Tóm lại: Nếu trong hàm constructor mà không có gì để gây ra leave cả, thì các bạn cứ xài như đã từng xài trước đây, nghĩa là dùng code như C++ vậy. Nếu trong hàm constrcutor này mà có leave thì bạn mới phải cần đến two-phase construction.

Chuỗi trên Symbian: Descriptor

Trên Symbian, chuỗi được gọi là descriptor bởi chúng tự mô tả (describe): một descriptor chứa kích thước, kiểu chuỗi bên cạnh nội dung chuỗi trong vùng nhớ. Đây là điều mà chuỗi trong C/C++ hay Java không có.
Descriptor được xây dựng theo hướng tối ưu hóa vùng nhớ cấp phát, điều rất cần trên các môi trường bộ nhớ thấp như Symbian. Nhưng cũng chính vì điều này mà descriptor làm đau đầu khá nhiều lập trình viên về việc sử dụng chúng.
Từ version 5.0 trở về trước, Symbian chưa hỗ trợ Unicode nên descriptor chỉ hỗ trợ chuỗi với ký tự 8 bit, nhưng từ v5.1 trở về sau thì descriptor trên Symbian hỗ trợ cả 8 bit và 16 bit (Unicode). Một điểm đặc biệt nữa là descriptor không sử dụng ký tự đặc biệt để kết thúc chuỗi như trên C/C++ hay Java, nên dữ liệu chúng chứa có thể là dữ liệu nhị phân. Việc sử dụng chung descriptor cho lưu trữ ký tự và nhị phân rõ ràng sẽ làm nhỏ gọn lại Symbian và tiện lợi cho người dùng. Lưu ý là khi muốn lưu trữ nhị phân, bạn phải khai báo descriptor ở dạng 8 bit.

1. Descriptor hằng:
Tất cả các kiểu descriptor trên Symbian đều kế thừa TDesC (typedef của TDesC16, xem e32std.h) và được định nghĩa trong e32des16.h (thư mục: epoc32\include). Bản 8 bit là TDesC8 (e32des8.h). Như đã giới thiệu: C kết thúc lớp báo hiệu đây là lớp hằng, dữ liệu không đổi.
Trên vùng nhớ , một descriptor được lưu trữ như sau: 4 byte đầu lưu trữ chiều dài (thật ra chỉ 28 bit trong số 32 bit (4 byte) là lưu trữ chiều dài chuỗi, do đó chuỗi tối đa mà ta có thể chứa trong descriptor là 2^28 byte, 256 MB, 4 bit cao còn lại chứa kích thước của đối tượng descriptor, đây là cơ sở để xác định kiểu descriptor.
Với 4 bit chúng ta có thể phân biệt 16 kiểu descriptor nhưng trên Symbian chỉ có 5 loại). Phần tiếp theo sẽ lưu trữ dữ liệu descriptor hay con trỏ chỉ đến vùng lưu trữ thật sự.
Lấy chiều dài thông qua hàm Length(), trong khi truy cập dữ liệu thì nhở vào hàm Ptr(). Đây là 2 hàm ảo quan trọng nhất, từ đây TDesC sẽ cài đặt tất cả các hàm liên quan cho descriptor như truy cập dữ liệu, so sánh chuỗi, tìm kiếm, ...

2. Descriptor "động":
"Động" ở đây mang nghĩa là dữ liệu chưa trong descriptor có thể thay đổi. Tất cả các descriptor động đều kế thừa từ TDes. Về mặt lưu trữ, descriptor động giống như descriptor hằng, chỉ có thêm thành phần chứa chiều dài tối đa của chuỗi dữ liệu. Hàm MaxLength() sẽ trả về giá trị này.
TDes định nghĩa một loạt các phương thức phục vụ cho việc thay đổi dữ liệu như thêm, chèn, hay định dạng dữ liệu,... Một đặc điểm đáng lưu ý là một khi đã khai báo chiều dài tối đa thì không sao thay đổi được, do TDes và các lớp kế thừa từ nó đều không có hàm phục vụ cho việc cấp thêm bộ nhớ nên khi thêm dữ liệu, bạn phải lưu ý là dữ liệu thật sự lưu không bao giờ được vượt quá max length, nếu không bạn sẽ gặp phải lỗi.
Trên đây tôi đã giới thiệu về 3 lớp cơ bản về descriptor, TDes16, TDes8 và TDes (dạng trung tính dùng cho mục đích tương thích, hiện nay TDes luôn là TDes16 như tôi có nhắc ở trên). Đây là các lớp cơ sở của tất cả các lớp descriptor khác, chúng chủ yếu cung cấp các hàm thao tác cho các descriptor hằng cũng như động khác hơn là phục vụ cho mục đích lưu trữ dữ liệu như các lớp dẫn xuất khác. Do đó, bạn sẽ thấy chúng chủ yếu ở vai trò làm tham số hay kết quả trả về nhằm mục đích phục vụ cho lập trình tránh bị phụ thuộc loại descriptor cụ thể (đặc điểm độc đáo của lập trình hướng đối tượng). Một điểm nữa củng cố cho điều trên là bạn không thể khai báo trực tiếp một đối tượng kiểu TDesC hay TDes được do constructor của chúng là hàm protected.

Tiếp theo chúng ta sẽ tìm hiểu hai loại descriptor: buffer descriptor, với phần dữ liệu chứa trong descriptor sau phần chiều dài, và pointer descriptor với phần dữ liệu chứa nơi khác, sau phần chiều dài trong descriptor chỉ chứa địa chỉ vùng nhớ noi thật sự lưu dữ liệu.

rước khi tìm hiểu 2 loại descriptor, chúng ta sẽ tìm hiểu về literal descriptor, một loại descriptor đặc biệt.

1. Literal descriptor:
Đây là một loại descriptor hằng, gần giống với static char [] trên C. Chúng thường được dùng dưới dạng các macro gồm 3 dạng _LIT, _L và _S (đây là 3 dạng trung tính, thật sự là _LIT16, _L16, _S16, _LIT8, _L8, _S8 phục vụ chuỗi 16 và 8 bit) bạn có thể tìm thấy các khai báo macro này ở e32def.h.
- _LIT macro: khai báo như sau:
Code:

_LIT(KHello, "Hello World!");

Với khai báo như vậy, KHello được xem là tên 1 đối tượng TLITC16, nó sẽ chỉ đến một đoạn dữ liệu nhị phân của chương trình chứa nội dung cần lưu trữ, trong trường hợp này là chuỗi "Hello World!". Lưu ý là khi biên dịch, trên file nhị phân chương trình, sau chuỗi "Hello World!" có ký tự kết thúc chuỗi '\0'. Và thông thường file chương trình nạp trên ROM (các ứng dụng hệ thống) nên người ta thường nói là _LIT tạo chuỗi lưu trên ROM. Lúc này thông qua tên đối tượng TLIT16 là KHello, bạn có thể hoàn toàn sử dụng nó như là một descriptor hằng.
- _L macro: khai báo như sau:
Code:

_L("Hello World!")

;
Cũng giống như ở trên, với khai báo _L, một vùng dữ liệu trên file chương trình được dùng để chứa chuỗi khai báo, nhưng khác với ở trên, chúng không có tên, không có gì để nắm giữ, điều khiển chúng. Trong trường hợp này, hệ thống sẽ tạo một pointer descriptor là TPtrC trên stack (chúng ta sẽ nghiên cứu TPtr sau) tạm thời đảm nhận việc kiểm soát và xử lý chuỗi dữ liệu này. Hiện nay, Symbian đã đề xuất bỏ kiểu khai báo này do tốn thêm stack cho đối tương tạm TPtr và chi phí khởi tạo nó. Tuy nhiên nhờ ưu điểm là giảm code lại khỏi phải đặt tên nên chúng vẫn thường được dùng với mục đích test. Ví dụ, một khi ứng dụng lỗi, để test xem có phải hàm này gây ra hay không, trên UIQ chúng ta có thể làm theo cách sau: ngay sau hàm nghi ngờ, chúng ta đặt hàm sau:
Code:

User::InfoPrint(_L("Pass"));

Nếu dòng chữ "Pass" hiện lên màn hình thì rõ ràng lỗi chắc chắn nằm sau hàm này. Cách viết này dễ hơn rất nhiều so với:
Code:

_LIT(KPass, "Pass"); User::InfoPrint(KPass));

- _S macro: Gần giống với _L, tuy nhiên nó không yêu cầu tao đối tượng tạm TPtr mà sẽ cho phép sử dụng chuỗi trực tiếp như trên C.

2. Buffer descriptor:
Đây là descriptor mà chuỗi dữ liệu chứa trong đối tượng descriptor. Chúng được chia làm 2 loại dữ trên vùng nhớ lưu trữ: stack và heap.

a. Stack buffer descriptor:
Chúng có thể là descriptor hằng hoặc động. Chúng kế thừa từ TBufCBase và TBufBase gồm TBufC và TBuf. TBufC phục vụ cho các descriptor hằng, còn TBuf cho descriptor động, n ở đây chính là khai báo cho chiều dài tối đa của chuỗi. Chúng được so sánh với char [] trên C. Ví dụ:
Code:

_LIT(KHello, "Hello World!"); TBufC<12> aHelloBufC(KHello); TBuf<15> aHelloBuf(KHello);

Lưu ý: do kích thước stack cấp cho một ứng dụng là rất nhỏ nên chuỗi cấp trên stack cũng chỉ nên cấp cho các chuỗi nhỏ, thường dưới 128 byte, nếu lớn hơn nên cấp trên heap.

b. Heap buffer descriptor:
Loại descriptor này ra đời để phục vụ cho các descriptor có kích thước lớn, không thể lưu trên stack được, và đặc biệt hữu dụng trong các trường hợp không biết rõ kích thước tại thời điểm biên dịch. Nó được dùng chủ yếu cho mục đích lưu trữ dữ liệu trong các thao tác xử lý file hay trên các kênh truyền thông: hồng ngoại hay bluetooth.
Đại diện cho descriptor này là HBufC (HBufC16 hay HBufC8), nhưng thường được sử dụng thông qua con trot, HBufC*. Vởi "C" kết thúc lớp, chúng ta biết rằng, chúng là một loại descriptor hằng. Chúng cung cấp khá nhiều hàm NewL() để tạo các đối tượng descriptor. Lưu ý là sau khi dùng xong phải hủy chúng đi vì chúng được cấp tren heap. Ví dụ:
Code:

_LIT(KHello, "Hello World!"); HBufC* iHelloBufC = HBufC::NewL(20); *iHelloBufC = KHello;//Copy nội dung KHello vào iHellBufC

3. Pointer descriptor:
Đây là loại descriptor mã dữ liệu của chúng do một descriptor khác lưu giữ, có thể trên heap, stack hay trong ROM. Chúng bao gồm 2 loại, hằng và động được so sánh với const char* và char*, được thể hiện qua 2 lớp TPtrC và TPtr. Ví dụ:
Code:

_LIT(KHello, "Hello World!"); TPtrC aHelloBufC(KHello); TPtr aHelloBuf(KHello);

Sau đây là mô hình lưu trữ 2 kiểu descriptor và sơ đồ khái quát về descriptor:
Attached Thumbnails
Click image for larger version Name: desh1.jpg Views: 59 Size: 7.7 KB ID: 29540 Click image for larger version Name: desh2.jpg Views: 52 Size: 8.5 KB ID: 29544 Click image for larger version Name: desh3.jpg Views: 68 Size: 16.3 KB ID: 29545

RefLink: http://my.opera.com/nhadautu/blog/2008/01/06/chuong-trinh-hoat-dong-tren-symbian-2