Trao đổi với tôi

www.hdphim.info

11/28/10

[Symbian] Lập trình với Symbian OS P4

Cấu trúc chương trình trên Symbian S60


Bây giờ chúng ta đi sau phân tích cấu trúc của một chương trình trên Symbian S60 thông qua ví dụHelloWorld ở trên. Đây là ứng dụng dạng GUI có đuôi .APP. Để thống nhất tên lớp thì trên Symbian S60 3rd, bạn tạo Project với tên là Example chứ không phải HelloWorld như trước nữa.

Chúng ta thấy rằng, ứng dụng HelloWorld bao gồm các lớp như sau:
- Lớp CExampleApplication: Đây chính là lớp Application và kế thừa từ CEikApplication (trên Symbian S60 1st) hoặc lớp CAknApplication (trong Symbian S60 3rd). Lớp này chịu trách nhiệm thiết lập và thực thi ứng dụng.
- Lớp CExampleDocument: Đây là lớp Document của ứng dụng. Lớp này được kế thừa từ lớpCEikDocument (trên Symbian S60 1st) hoặc lớp CAknDocument (trên Symbian S60 3rd). Lớp này được tạo bởi lớp Application, nó quản lý dữ liệu của ứng dụng. Lớp này cũng có nhiệm vụ khởi tạo lớp giao diện ứng dụng AppUi.
- Lớp CExampleAppUi: Đây là giao diện ứng dụng AppUi. Lớp này kế thừa từ CEikAppUi (Symbian S60 1st) hoặc CAknAppUi (Symbian S60 3rd), và có nhiệm vụ điều khiển các sự kiện. Lớp này sẽ nhận các sự kiện như chọn menu, phím bấm, … và gửi sự kiện này tới các ViewContainer.
Lớp này điều khiển sự kiện chọn menu thông qua hàm
MÃ: CHỌN TẤT CẢ
void HandleCommandL(TInt aCommand);

và điều khiển sự kiện phím nhấn thông qua hàm:
MÃ: CHỌN TẤT CẢ
void HandleWsEventL(const TWsEvent &aEvent, CCoeControl *aDestination)

- Lớp CExampleAppView: Đây chính là view để hiển thị ra màn hình, nó kế thừa từ lớp CCoeControl. Bạn hãy thực hiện thao tác vẽ trong hàm:
MÃ: CHỌN TẤT CẢ
void Draw(const TRect& /*aRect*/) const;

của lớp view.

Như vậy chúng ta thấy cấu trúc ứng dụng GUI trên Symbian S60 như sau:



Khi ứng dụng được chọn thực thi, chương trình apprun.exe sẽ hoạt động với tên ứng dụng và tên file ứng dụng làm tham số. Chương trình apprun sẽ sử dụng kiến trúc ứng dụng APPARC để nạp ứng dụng qua việc kiểm tra UID2KUiApp (0x100039ce) và tạo đối tượng ứng dụng đồ họa qua hàm NewApplication(). Các hàm này bao gồm hàm [color=#0000FF]NewApplication()[/color] và hàm E32Dll(TdllReason) (một hàm chỉ cài đặt, không sử dụng). Từ khóa EXPORT_C để báo hàm NewApplication là đầu vào của một DLL.
MÃ: CHỌN TẤT CẢ
EXPORT_C CApaApplication* NewApplication()
{
return new CExampleApplication;
}
GLDEF_C TInt E32Dll(TDllReason)
{
return KErrNone;
}

Khi NewApplication() được gọi, đối tượng CExampleApplication sẽ được tạo, đối tượng này tạoCExampleDocument, và lớp CExampleDocument sẽ tạo lớp giao diện ứng dụng CExampleAppUi. Khi lớp CExampleAppUi được tạo, hàm khởi tạo ConstructL của AppUi sẽ được gọi, và trong hàm này lớpCExampleAppView được tạo thông qua lệnh:
MÃ: CHỌN TẤT CẢ
iAppView = CExampleAppView::NewL(ClientRect());

Khi view được tạo, ngoài các hàm khởi tạo ContructL, nó gọi thêm hàm
MÃ: CHỌN TẤT CẢ
void Draw(const TRect& /*aRect*/) const;

để thực hiện vẽ lên màn hình hiển thị.

Trong lớp CExampleAppUi, thì bạn dùng hàm
MÃ: CHỌN TẤT CẢ
void HandleCommandL(TInt aCommand);

để điều khiển sự kiện cho menu. Nhưng trong ứng dụng trên Symbian S60 1st của chúng ta không có menu nên bạn không thể thoát khỏi ứng dụng. Bây giờ tôi thực hiện điều khiển sự kiện phím nhấn để thực hiện thoát khỏi ứng dụng khi nhấn phím trái. Ở đây tôi sử dụng hàm HandleWsEventL để điều khiển nhận sự kiện phím trái để thoát khỏi ứng dụng. Để thực hiện được điều này, bạn làm như sau:
- Bạn thêm khai báo vào phần khai báo của lớp CExampleAppUi:
MÃ: CHỌN TẤT CẢ
void HandleWsEventL(const TWsEvent &aEvent, CCoeControl *aDestination);

ngay sau khai báo hàm HandleWsEventL
- Trong phần mã lệnh của lớp CExampleAppUi bạn thêm đoạn mã sau:
MÃ: CHỌN TẤT CẢ
void CExampleAppUi::HandleWsEventL(const TWsEvent &aEvent, CCoeControl *aDestination)
{
if (aEvent.Type()==EEventKey && aEvent.Key()->iCode==EKeyDevice0)
Exit();

CEikAppUi::HandleWsEventL(aEvent, aDestination); // Tren Symbian S60 1st
//CAknAppUi::HandleWsEventL(aEvent, aDestination); // Tren Symbian S60 3rd
}

Bây giờ bạn chạy ứng dụng, và dùng phím chức năng bên trái để thoát khỏi ứng dụng. Việc bạn bắt sự kiện phím trên Symbian OS 3rd cũng tương tự.

+ Mô hình MVC trong Symbian
Như vậy một ứng dụng GUI trên Symbian tuân theo mô hình MVC (Model - View - Control). Mô hình này bao gồm:
- Model: Dữ liệu của ứng dụng, nó đảm nhận việc lưu trữ các thông tin dữ liệu của ứng dụng.
- View: Nơi thể hiện dữ liệu của ứng dụng, người dùng chỉ có thể biết ứng dụng thông qua nó.
- Controller: Phần này có nhiệm vụ thao tác trên dữ liệu ứng dụng: cập nhật model sau đó yêu cầu view thể hiện lại phần cập nhật.
=> Tuy nhiên không phải lúc nào cũng phải đầy đủ cả phần này. Tùy theo tính chất của ứng dụng mà có thể ranh giới giữa modelview không rõ ràng hay có thể thiếu đi phần controller. Đây chính nguyên tắc cho các ứng dụng đồ họa: "Những gì thể hiện trên màn hình giao tiếp với người dùng sẽ do các hàm vẽ (draw) đảm nhận. Chúng chỉ có nhiệm vụ đơn giản là vẽ lại dữ liệu của ứng dụng lên màn hình, chúng không thay đổi dữ liệu. Nếu bạn muốn thay đổi gì đó thì bạn phải sử dụng hàm khác và rồi gọi lại hàm vẽ này để vẽ lại dữ liệu đã thay đổi".

Mô hình MVCSymbian có mối quan hệ rất mật thiết, nó là mô hình thiết kế chủ đạo trong Symbian. Nếu để ý kỹ bạn sẽ thấy lớp document là hiện thân của model, lớp AppView chính là view, còn AppUi sẽ đóng vai trò là controller trong mô hình MVC. Với những ứng dụng phức tạp thì sẽ có nhiều lớp đảm nhận một thành phần trong MVC.

Không những vậy, hầu hết các control trong Symbian đều được thiết kế theo mô hình MVC. Nắm bắt được điều này, bạn sẽ dễ dàng thao tác với các control trong Symbian. Tôi nhớ là có khá nhiều người mới lập trình Symbian khi làm quen với listbox đều đặt câu hỏi: "Làm sao để lấy dữ liệu một item trong listbox đây bởi trong lớp CEikListbox không thể tìm thấy hàm nào đảm nhận việc này". Đó là vì listbox trong Symbian cũng được thiết kế theo mô hình MVC nên nếu muốn lấy dữ liệu, bạn phải đến lớp MListBoxModel thông qua hàm Model() trong lớp CEikListbox.

Quy ước và cách đặt tên khi lập trình với Symbian OS


Symbian đưa ra một số quy ước trong lập trình. Một số quy ước bạn không nhất thiết phải theo nhưng nhưng một số thì bạn nên tuân thủ để phục vụ cho việc lập trình của bạn thuận lợi, tránh sai sót và dễ nâng cấp sau này.

1. Tên lớp
Symbian sử dụng các quy ước đặt tên sau để xác định đặc tính cơ bản của một lớp:
- Lớp T: Lớp đơn giản (tựa như typedef) thường xây dựng từ các kiểu dữ liệu cơ sở hay kết hợp chúng lại, có thể so sánh lớp T với struct đơn giản bao gồm các dữ liệu public. Nó không có destructor (có thể cóconstructor nhưng hiếm) và thường được lưu trên stack, có thể lưu trên heap. Kiểu liệt kê (enum) cũng thường khai báo dưới dạng lớp T. Ví dụ: TInt, TBool, TPoint, TDes, TMonthsOfYear ...
- Lớp C: Lớp có constructordestructor và tất cả đều là dẫn xuất từ CBase. Các đối tượng của chúng được tạo bằng new và luôn được lưu trữ trên heap. Ví dụ: CConsoleBase, CActive,...
- Lớp R: Lớp R (đại diện cho Resource), thường đại diện cho một loại tài nguyên, quản lý một sesion kết nối với một server phục vụ một tài nguyên. Đối tượng lớp R thường có một hàm khởi tạo (Open() hoặcCreate() hay Initialize()) và một hàm kết thúc (Close() hay Reset()) để giải phóng tài nguyên. Quên gọi hàm kết thúc khi dùng đối tượng lớp R là một lỗi thường gặp và kết quả là sẽ bị leak bộ nhớ. Nó có thể được lưu trên heap, nhưng thường là trên stack. Ví dụ: RFile, RTimer, RWindow,...
- Lớp M (Mix-ins): Lớp ảo (abstract) giống như interface trong Java, nó chỉ bao gồm các phương thức ảo rỗng và không có dữ liệu cũng như constructor. Việc kế thừa nhiều lớp trong Symbian là cho phép tuy nhiên phải theo quy tắc là kế thừa chính từ 1 lớp C (bắt buộc phải có và viết đầu tiên) và nhiều lớp M. Ví dụ: MGraphicsDeviceMap, MGameViewCmdHandler,...
=> Việc phân biệt giữa T, CR lớp là rất quan trọng, nó ảnh hưởng tới việc giải phóng bộ nhớ khi sử dụng cũng như cách thức xử lý các đối tượng thuộc các lớp này.
Ngoài ra trong Symbian còn có các lớp tĩnh (static) phục vụ cho một số chức năng riêng như lớp User hay lớp Mem. Một ngoại lệ khác là lớp HBufC, chúng ta sẽ nói đến nó trong bài viết về sử dụng xâu trên Symbian.

2 Tên dữ liệu
Tương tự, Symbian cũng dùng chữ cái đầu để phân biệt các loại dữ liệu:
- Hằng liệt kê (Enumerated constant): Bắt đầu với ký tự E, nó đại diện cho một giá trị hằng trong một dãy liệt kê. Nó có thể là một phần của lớp T. Ví dụ: (ETrue, EFalse) của TBool hay EMonday là một thành phần của TDayOfWeek.
- Hằng (constant): Bắt đầu với ký tự K, thường được dùng trong các khai báo #define hay các giá trị hằng do Symbian quy định. Ví dụ: KMaxFileName hay KErrNone.
- Biến thành phần (member variable): Bắt đầu với chữ cái i (instance), được dùng khi sử dụng các biến động là thành viên của một lớp. Đây là quy ước quan trọng, dùng cho việc hủy vùng nhớ trên heap của các đối tượng này trong destructor. Tôi thường chỉ dùng quy ước này nếu biến này sẽ được lưu trên heap, còn trên stack thì không. Ví dụ: iDevice, iX, …
- Tham số (argument): Bắt đầu bằng chữ a (argument), được dùng khi các biến làm tham số. Ví dụ:aDevice, aX, …
- Macro: Không có quy ước đặc biệt. Tất cả đều viết hoa và dùng dấu gạch dưới để phân tách từ. Ví dụ:IMPORT_C, _TEST_INVARIANT, _ASSERT_ALWAYS, v.v…
- Biến cục bộ (automatic): Chữ cái đầu nên viết thường.
- Biến toàn cục (global): Nên viết hoa chữ cái đầu nhưng để tránh nhầm lẫn nên bắt đầu tên bằng chữ cái “g”. Tuy nhiên trên Symbian không khuyến khích dùng biến toàn cục.

3. Tên hàm
Tên hàm bắt đầu bằng ký tự hoa. Khác với 2 trường hợp trên, quy ước đặt tên hàm lại dựa trên ký tự cuối cùng:
- Hàm không ngắt giữa chừng (non-leaving function): Đó là hàm mà trong quá trình thực thi nó đều diễn ra suông sẻ, chi tiết tôi sẽ nói sau. Ví dụ: Draw() hay Intersects().
- Hàm ngắt giữa chừng (leaving function): Là hàm bị ngắt ngang vì một lý do nào đó: lỗi, thiếu tài nguyên, ... Hàm này kết thúc bằng ký tự L. Ví dụ: DrawL() hay RunL().
- Hàm LC: Kết thúc với cặp ký tự LC. Các hàm này trong lòng nó có khai báo một đối tượng mới, và có đặt đối tượng này lên cleanup stack (ngăn xếp chứa các đối tượng cần xóa khi có ngắt xảy ra, sẽ nói rõ sau) và có khả năng xuất hiện ngắt trong khối xử lý hàm. Bạn lưu ý là sau khi gọi hàm này sẽ phải gọiCleanup:PopAnd Destroy(), lý do tôi sẽ nói trong phần về cleanup stack, nếu quên gọi nó chắc chắn bạn sẽ bị lỗi 3 mà không hiểu tại sao. Ví dụ: AllocLC(), CreateLC(), OpenLC() hay NewLC(),...
- Các hàm Get và Set: Trong trường hợp đơn giản thường là các hàm thành viên của một lớp. Set dùng cho việc xác lập giá trị cho một biến thành viên của lớp. Get được dùng cho các hàm sẽ trả về giá trị trên tham số. Khi hàm có giá trị trả về thì thường không dùng Get.
Ví dụ: SetThing(aData); GetThing(aData); nhưng iData = Thing();

4. Cấu trúc thư mục project
Tuy không bắt buộc nhưng Symbian khuyến khích lập trình viên xây dựng thư mục project ứng dụng thành các thư mục con với chức năng riêng biệt. Thông thường thư mục project có cấu trúc như sau:
- Thư mục project: Chứa các file project .mmp, bld.inf, file tài nguyên .rss. Thư mục này cũng sẽ lưu trữ các file thông tin cụ thể cho chương trình dùng với các IDE. Thư mục này thường được đặt tên là: group.
- Thư mục các file khai báo: Chứa các file khai báo cho file tài nguyên và các file mã nguồn. Thư mục này thường có tên là: inc.
- Thư mục mã nguồn: Chứa các file cài đặt các lớp chương trình. Thư mục này thường có tên là: src.
- Thư mục dữ liệu: Chứa dữ liệu cần cho chương trình ứng dụng và có tên là data.
- Thư mục thông tin ứng dụng: Chứa file tài nguyên .rss để tạo file .aif và các hình ảnh, tài nguyên phục vụ cho ứng dụng. Tậo hợp các hình này được lưu trữ trong một file .mbm (multi bitmap). Thư mục này có tên là aif.
- Thư mục cài đặt: Chứa các file .pkg, các thành phần cài đặt bổ sung cho ứng dụng. Thư mục này thường có tên là: install.
- Thư mục chương trình: Lưu trữ file cài đặt .sis. Thường nó được gộp với thư mục install. Thư mục này có tên là release.
=> Tuy nhiên các project thường chỉ gồm các thư mục: group, incsrc.

5. Trình bày code
Nếu lập trình trên C và Java thấy 2 phong cách trình bày quen thuộc là:
MÃ: CHỌN TẤT CẢ
void Example()
{
.........
.........
}


MÃ: CHỌN TẤT CẢ
void Example(){
.........
.........
}

thì Symbian đề xuất cách trình bày sau:
MÃ: CHỌN TẤT CẢ
void Example()
->{
->.........
->........
->}

Bạn không nhất thiết phải theo cách này. Tôi thì quen theo phong cách của C hơn :)

Xử lý ngoại lệ trên Symbian (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 TRAPTRAPD trên Symbian thì tương đồng với trycatch trên C++.
Ví dụ:
MÃ: CHỌN TẤT CẢ
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 như 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: gây ra "leak" bộ nhớ.

An toàn hơn với Cleanup stack


Khi 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àmClose() 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 leak (lỗ hổng) trên bộ nhớ.

Leak bộ nhớ là vùng nhớ trên heap thực sự không được sử dụng nhưng hệ điều hành nghĩ là nó đang sử dụng và sẽ không sử dụng vùng nhớ này để cấp phát cho các đối tượng khác. Leak bộ nhớ thường do bạn cấp phát động bộ nhớ (sử dụng hàm new, ...) mà không giải phóng nó (hàm delete,...). Bị leak bộ nhớ sẽ gây ra sự lãng phí tài nguyên bộ nhớ, đặc biệt trên thiết bị giới hạn về tài nguyên như di động thì là cả một vấn đề.

Ví dụ:
MÃ: CHỌN TẤT CẢ
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, stacksẽ 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:
MÃ: CHỌN TẤT CẢ
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 số lưu ý:
- 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àmCleanupStack::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ớpCleanupStack).
- 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.


LeaveCleanup 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.

Khởi tạo hai pha (Two-phase construction) trong Symbian


Đế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()NewLC() để khởi tạo đối tượng không. Nhận thấy có một phần quan trọng đi liền sau leavecleanup stack là khởi tạo 2 pha (two-phase construction) nên tôi sẽ xin nói về nó luôn.

1. Khởi tạo hai pha (Two-phase construction)
Theo đúng cú pháp C++, một đối tượng mới sẽ được cài đặt như sau:
MÃ: CHỌN TẤT CẢ
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ớpCExam 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 bạn sẽ nghĩ ngay đến cleanup stack, 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.
MÃ: CHỌN TẤT CẢ
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()NewLC() như sau:
MÃ: CHỌN TẤT CẢ
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:
MÃ: CHỌN TẤT CẢ
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.
MÃ: CHỌN TẤT CẢ
CExam* exam = new 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 constructor này mà có leave thì bạn mới phải cần đến two-phase construction.

Sử dụng chuỗi (Descriptor) trên Symbian


+ Tổng quan về chuỗi (Descriptor) trên Symbian
Trên Symbian, chuỗi được gọi là descriptor bởi chúng tự mô tả: 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.

Các descriptor có hai dạng:
- Descriptor hằng (Descriptor có dữ liệu không thể thay đổi được)
- Descriptor động (Descriptor có dữ liệu có thể thay đổi được)


Hình vẽ sau cho cho ta thấy được kiến trúc của Descriptor trên Symbian:


* Descriptor hằng TDesC
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, ...

Đây là lớp trừu tượng nên nó không thể khởi tạo, vì vậy khi sử dụng lớp này làm tham số cho hàm bạn sử dụng cú pháp dạng const TDesC& để cung cấp các truy cập chỉ đọc tới dữ liệu của Descriptor.

* Descriptor "động" TDes
"Độ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.

TDesC, TDes (TDes16, TDes8 - Hiện nay TDes luôn là TDes16). Đâ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 [color=#BF0000]TDes[/color] được doconstructor của chúng là hàm protected.

+ Một số Descriptor cụ thể
* Xâu dạng Literals (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_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.

Literals cung cấp cách đơn giản để định nghĩa xâu trong chương trình. Chúng ta có thể định nghĩa và sử dụng như sau:
MÃ: CHỌN TẤT CẢ
_LIT(KMyName, "Helen");
TBuf myName;
myName.Append(KMaxItemLength);

Literals rất dễ thay đổi nếu bạn định nghĩa chúng ở đầu file .cpp, chúng chỉ đơn giản như định nghĩa marco.

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.
MÃ: CHỌN TẤT CẢ
myName.Append(_L("Helen"));

Trong trường hợp này, hệ thống sẽ tạo một pointer descriptorTPtrC 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. . 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:
MÃ: CHỌN TẤT CẢ
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:
MÃ: CHỌN TẤT CẢ
_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 tạo đối tượng tạm TPtr mà sẽ cho phép sử dụng chuỗi trực tiếp như trên C.

=> 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. _LIT cần ít bộ nhớ hơn nếu xâu được sử dụng nhiều hơn 1 lần và Symbian khuyến khích sử dụng _LIT trong việc tạo các ứng dụng sử dụng hiệu quả vùng nhớ.

* 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ữ: stackheap.
- Stack buffer descriptor: Chúng có thể là descriptor hằng hoặc động. Chúng kế thừa từ TBufCBaseTBufBase gồm TBufCTBuf. 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ụ:
MÃ: CHỌN TẤT CẢ
_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.

- 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 (HBufC16hay HBufC8), nhưng thường được sử dụng thông qua con trỏ, HBufC*. Bạn có thể dùng 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 trên heap. Ví dụ:
MÃ: CHỌN TẤT CẢ
_LIT(KHello, "Hello World!");
HBufC* iHelloBufC = HBufC::NewL(20);
*iHelloBufC = KHello; //Copy nội dung KHello vào iHellBufC

Kí tự ‘C’ xác định descriptor là hằng và không thể chỉnh sửa được. Nếu bạn cần chỉnh sửa descriptor này bạn có thể sử dụng hàm Des(), hàm này khởi tạo một con trỏ trỏ tới descriptor này, là con trỏ dạng TPtrvà có thể chỉnh sửa được dữ liệu. Ví dụ:
MÃ: CHỌN TẤT CẢ
_LIT(KMessage, "Hello");
HBufC* aMessage = HBufC8::NewL(KMaxItemLength);
aMessage->Des().Append(KMessage);

Để có thể thao tác với xâu có kích thước lớn hơn, bạn có thể sử dụng ReAllocL() để mở rộng kích thước của Desciptor nhưng khi đó có thể xảy ra lỗi tràn hay gây ra một panic.
MÃ: CHỌN TẤT CẢ
aMessage = aMessage->ReAllocL(25);

Bình thường các descriptor cấp phát trên vùng heap có thể khởi tạo bằng New() hoặc NewLC(). Nhưng nếu descriptor này đã tồn tại, thì có thể dùng các phương thức Alloc(), AllocL(), AllocLC() để tạo descriptor mới.
MÃ: CHỌN TẤT CẢ
TBuf temp;
temp.Append(_L("Hello Mum"));
HBufC* hPtr;
hPtr = temp.AllocL();


* Pointer descriptor: Đây là con trỏ trỏ tới Descriptor khác. Chúng bao gồm 2 loại hằngđộng được so sánh với const char*char*, được thể hiện qua 2 lớp TPtrCTPtr. Ví dụ:
MÃ: CHỌN TẤT CẢ
_LIT(KHello, "Hello World!");
TPtrC aHelloBufC(KHello);
TPtr aHelloBuf(KHello);

- TPtrC là kiểu hằng, kí tự ‘C’ trong TPtrC là viết tắt của từ Constant, có nghĩa là không thể thay đổi. Con trỏ này có hai tham số là độ dài và địa chỉ:


- Trong khi đó TPtr có thêm tham số xác định độ dài tối đa Descriptor cho phép chỉnh sửa (Max length)


+ Các phương thức thao tác với Descriptor
* Các phương thức cho descriptor hằng
Sau đây tôi xin giới thiệu một vài phương thức thông dụng sử dụng cho lớp TDesC and TBufC. Các phương thức khác bạn có thể tham khảo thêm trong tài liệu SDK.
- Alloc(), AllocL()AllocLC(): Tạo desciptor 16 bit trên vùng heap và chứa bản sao của dữ liệu.
- Compare(), CompareC()CompareF(): So sánh hai xâu và trả về giá trị một số nguyên. Chú ý kí tự ‘C’ chỉ “collated comparisons” còn kí tự ‘F’ chỉ ‘folded comparisons’. Giá trị trả về của phương thức so sánh:
0: Hai xâu bằng nhau.
Giá trị dương: Descriptor chính lớn hơn tham số.
Giá trị âm: Descriptor chính nhỏ hơn tham số.

MÃ: CHỌN TẤT CẢ
TBufC descriptorOne;
descriptorOne = _L("One");
TBufC descriptorTwo;
descriptorTwo = _L("Two");
TInt result;
result = descriptorOne.Compare(descriptorTwo);

Biến result có giá trị -5 nhưng hiếm khi ta quan tâm tới giá trị này mà ta chỉ quan tâm tới dấu của giá trị trả về của hàm so sánh.
- Find(), FindC()FindF(): Tìm kiếm xâu con. Giá trị trả về là một số nguyên xác định offset tới thể hiện đầu tiên của xâu tìm được. Nếu không tìm thấy, giá trị KErrNotFound được trả về. Các hàm này luôn tìm từ vị trí bắt đầu của xâu, để tìm từ một vị trí khác bạn sử dụng thêm các hàm Left(), Mid(), Right().
- Left(TInt aLength): Trả về một TPtrC tới phần tận cùng bên trái của descriptor.
- Right(TInt aLength): Trả về một TPtrC tới phần tận cùng bên phải của descriptor.
- Mid(TInt aPos): Trả về một TPtrC tới một vùng xác định của xâu, bắt đầu từ vị trí aPos và kết thúc ở cuối xâu.
- Mid(TInt aPos, TInt aLength): Trả về một TPtrC tới một vùng xác định của xâu, bắt đầu từ vị trí aPos với độ dài xác định bởi aLength.
- Length(): Trả về số kí tự trong xâu.
- Size(): Trả về số byte của Descriptor.
- Locate(), LocateC()LocateF(): Tương tự như phương thức Find() nhưng để tìm kí tự đơn chứ không phải tìm một xâu. Vị trí kí tự đầu tiên chính là vị trí đầu của xâu. Ví dụ sau trả về giá trị 1 trong biến result:
MÃ: CHỌN TẤT CẢ
TBufCdescriptorOne;
descriptorOne = _L("One");
TChar aChar;
aChar = 'n';
result = descriptorOne.Locate(aChar);

- LocateReverse(): Hàm này tương tự như Locate() nhưng tìm từ vị trí cuối xâu. Nếu không tìm thấy thì giá trị KErrNotFound được trả về.
+ Toán tử !=, <, >, <=, >=, == và []: Giúp đơn giản trong so sánh xâu. Các toán tử này được định nghĩa trong TDesC. Trong TBufC đưa ra toán tử =, mặc dù đây là lớp hằng.
MÃ: CHỌN TẤT CẢ
TBufC<5> descriptorOne;
descriptorOne = _L("One");
TBufC<5> descriptorTwo;
descriptorTwo = _L("Two");
if (descriptorOne <= descriptorTwo)
descriptorTwo = descriptorOne;

* Các phương thức thao tác với desciptor "động"
Descriptor TBufTPtr đưa ra các phương thức giúp bạn có thể chỉnh sửa được dữ liệu.
- Append(): Giúp chèn thêm một kí tự hay một descriptor.
MÃ: CHỌN TẤT CẢ
TBuf aDescriptor;
aDescriptor.Append(_L("Hello"));

Nếu độ dài của descriptor mới và dữ liệu hiện tại của aDescriptor mà lớn hơn KMaxLength, ứng dụng sẽ sinh ra USER 11 PANIC.
- Capitalize(), UpperCase() và LowerCase(): Tên của hàm đã thể hiện rõ chức năng của các hàm này.
- Copy(): Hàm này copy từ descriptor nguồn tới descriptor đích, và thay thế hết các dữ liệu đã tồn tại.
- Delete(): Xóa các vùng con xác định của xâu. Ví dụ sau khi thực hiện lệnh sau thì aDescriptor chứa“Hello Peace”:
MÃ: CHỌN TẤT CẢ
TBuf<20> aDescriptor;
aDescriptor.Append(_L("Hello World Peace"));
TChar aChar = 'W';
TInt aPos = aDescriptor.Locate(aChar);
TInt aLength = 6;
aDescriptor.Delete(aPos, aLength);

- Fill(): Hàm này điền đầy descriptor bằng một kí tự nào đó hay là kí tự có mã 0.
- Format(): Sao chép dữ liệu đã được định dạng vào descriptor. Chi tiết xem thêm tài liệu SDK.
MÃ: CHỌN TẤT CẢ
aDescriptor.Format(_L("%S\t%S"), aDes1, aDes2);

- Num(TInt aNum): Chuyển số nguyên sang xâu và đưa vào descriptor. Nếu descriptor đã có dữ liệu thì nó thay thế dữ liệu đã tồn tại.

No comments:

Post a Comment