Enhydra 的 kXML 是佔用極小儲存空間的 XML 解析器,極適合 J2ME 應用程式。它擁有獨特的 DOM 操作方法與稱為 Pull 的解析方法。
開發一個用於 J2ME 設備的多人遊戲專案。原本伺服器與設備間通訊編碼為以「&」分隔的鍵值對,檢索變數快,但處理複雜、巢狀資料結構時不適用,寫入資料困難且易出錯。為解決此問題,決定用 XML 重寫資料傳輸部分。XML 是自然選擇:不僅曾用於前專案透過網路向 Applet 傳送資訊,且易於除錯、編寫,還能用豐富格式結構化資料。意外發現 kXML 這顆編程工具箱的珍貴寶石。
kXML 是為 J2ME 設備設計的簡化類庫,亦可用於其他需小型 XML 解析器的環境(如 Applet)。Enhydra 維護專案,支援:
本文詳細說明 Pull 解析與 DOM 操作特性,並示範如何檢查 kXML 記憶體操作效果。兩個 MIDlet 範例含完整原始碼(點擊下載)。KToolBar 1.04 專案不含 kXML 類庫——必須從 https://kxml.enhydra.org/ 取得,解壓縮放入專案「lib」目錄。
<html>,監聽器接收事件通知並傳遞所需資訊。此策略解析器稱為 Push 解析器,因將事件「推入」監聽器。kXML 支援 DOM 解析與操作,但不支援 Push 解析。取而代之,使用略有不同的「Pull」解析方法。與 Push 相反,Pull 解析讓程式設計師從解析器「拉」出下一事件。Push 解析中,必須維護正在解析的當前資料狀態,基於傳送到監聽器的事件恢復先前狀態、切換狀態時保存新狀態。Pull 解析使處理狀態變更更容易,因可發送解析器到不同函數,維護它們自己的狀態變數。
範例程式 KXMLDemo_Pull 使用 Pull 解析器查看包含通訊錄資訊的檔案。關鍵程式碼片段:
XmlParser parser = null;
parser = new XmlParser(new InputStreamReader(this.getClass().getResourceAsStream(resfile_name)));
建立 XmlParser 傳入 InputStream。解析器反覆調用直到出現 END_DOCUMENT 事件:
while ((event = parser.read()).getType() != Xml.END_DOCUMENT) {
if (name != null && name.equals("address")) {
parseAddressTag(parser);
}
}
判斷事件是否以 <address> 標籤開始,傳送解析器到控制解析的 parseAddressTag:
while ((event = parser.peek()).getType() != Xml.END_DOCUMENT) {
if (type == Xml.END_TAG && name.equals("address")) {
return;
}
ParseEvent next = parser.read();
// 非文字事件則跳過
if (next.getType() != Xml.TEXT) continue;
System.err.println(name + ": " + text);
}
在 parseAddressTag 中循環直到找到對應 </address> 結束標籤。遇到其他標籤,標籤名與內容列印到控制台。如發現 <name>Robert Cadena</name>,輸出:
name: Robert Cadena
一旦找到 </address> 結束標籤,控制權返回調用函數,繼續尋找下一個 <address>。可見 Pull 解析器易用,且能傳送解析器到另一函數在文檔中尋找元素。不限於解析資源檔,亦可用 HttpConnection 傳遞至 HTTP InputStream,從讀取、儲存、解析 InputStream 等操作中解脫,一切由 kXML 完成。
Pull 解析特別適合需維護極小儲存空間時,因產生事件的文檔僅部分存在記憶體。若感興趣的特定資料段在文檔中部數百位元組,前面數百位元組無須保留記憶體。
若記憶體允許,可用含 DOM 支援的另一版 kXML 解析器。DOM 是記憶體中完整文檔樹,每個標籤分離為節點物件。可遍歷文檔樹按需取得資料。
另一 MIDlet KXMLDemo_dom 做相同事:讀取通訊錄、列印內容,但使用 DOM。關鍵程式碼:
Document doc = new Document();
parser = new XmlParser(isr);
doc.parse(parser);
建立 Document 保存 XML 樹。從 InputStreamReader 建立 kXML 解析器,傳送給文檔開始解析。XML 遞迴解析至文檔結尾。解析調用退出時,整個文檔載入記憶體,可開始操作。
Element root = doc.getRootElement();
int child_count = root.getChildCount();
for (int i = 0; i < child_count; i++) {
Element kid = root.getElement(i);
if (!kid.getName().equals("address")) continue;
int address_item_count = kid.getChildCount();
for (int j = 0; j < address_item_count; j++) {
// 遍歷子元素列印內容
}
}
因知 <address> 為根元素直接子元素,遍歷根元素子元素尋找 address 標籤。找到後遍歷其子元素列印內容。註:不能直接用 kid.getElement("name"),若元素不存在會拋 RuntimeException,建議僅在確知 XML 文檔含所有所需欄位時使用。
經驗法則:無法確保應用結構且需保持記憶體佔用低時用 Pull 解析器;有足夠記憶體且可能需透過新增/移動標籤操作文檔時用 DOM。
若想看兩種方法記憶體使用情況,在 KToolBar 開啟專案,使用記憶體監視器(Edit → Preferences: Monitoring Tag)。運行 MIDlet 彈出記憶體監控視窗,圖形與數字顯示記憶體使用。觀察綠線上升代表應用消耗記憶體量。Pull 應用比 DOM 應用少用一些記憶體。雖本例兩 MIDlet 差異不大,但若 MIDlet 用 DOM 遍歷更大檔案,將消耗更多記憶體。