2015年11月21日土曜日

[安藤]android將2MB的XML匯入DB,耗時由10分鐘 ->6秒的歷程



這次接到的任務是下載一個壓縮過的xml檔案,並灌入db裡面。
檔案下載之前已經寫好含db+cache管理功能的模組,zip也只要套個zipinputstream並預先分析zipentry是不是目標檔。
XML parser之前也已經用DOM格式寫好可以任意取text / attributes值的tool。
一切都是如此美好。



但是在遇到2MB的xml之後,完全變了調。
初次的完整執行,等了3分鐘開始不耐煩,以為跑進了無限迴圈。
開始加一些debug print,發現一直有在動,只好耐心等待。

等了5分鐘,真的等不下去,分析一下debug print,資料的重複性似乎很高,
有點擔心是不是會跑不出來。轉而步進分析,看看到底是慢在哪。

一開始步進,傻了...



  DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
  DocumentBuilder db;
  try {
   db = dbf.newDocumentBuilder();
   InputSource ii = new InputSource();
   ii.setCharacterStream(is);
   Document doc = db.parse(ii);
   doc.getDocumentElement().normalize();
   return doc;
  } catch (ParserConfigurationException e) {
   // Auto-generated catch block
   e.printStackTrace();
  }


以上這幾行就要1分鐘(debug狀態)。 Orz
常用的doc.getElementsByTagName(...); 一call就是幾十秒在算。
心想這不是辦法。趕快找資料求救。咕狗:「android xml dom parse too slow」
直到看到了這一篇:
http://stackoverflow.com/questions/7224318/how-to-solve-the-xml-parsing-performance-issue-on-android
才知道DOM parser之慢... Orz (上面那篇的圖表必見。)


看來把parser改寫成SAX格式是在所難免。還好之前有寫過並不會覺得太難,只是SAX的循序走查方式,對付xml階層很深的格式的xml很難寫。
目標的XML只有兩層,資料都用attributes帶入,就不需要考慮這樣的問題。
(開始覺得設計目標資料格式的人一定相當有經驗。)

改寫成sax格式之後再跑,果然是快多了。一口氣從10分鐘降為3分鐘。
不過還是在需要做task管理的範圍。結果又花了點時間寫暫停跟續傳。

但是,想要加速的想法並沒有這樣就停止。
看起來問題應該不是xml了。那剩下的就是在db的存取上面。
繼續查資料... 咕狗:「android db insert too slow」

又看到一篇:
http://stackoverflow.com/questions/3501516/android-sqlite-database-slow-insertion

介紹只要把db.insert(...)
用下面的方法寫

db.beginTransaction();
db.insert(...);
db.setTransactionSuccessful();
db.endTransaction();



就可以爆速。於是加了那三行。3分鐘變成30秒。 (抖)

30秒已經是可以不需要暫停背景執行的射程目標。
接下來就是繼續努力看看是不是可以再縮。
因為目前都還是使用單筆插入,於是把焦點放到bulkinsert(...)上面。
得一次全抓完資料再寫入,需要比較大的記憶體,為了速度這時也不在意了,
全部放到ArayList<ContentValues[]>去。

bulkinsert一樣加上上面三行,改成bulkinsert之後,降為10秒!
之前寫的task管理的操作正式宣告作廢。 雖然是好事啦。

繼續努力。上面那篇文中還提到一個技巧:


private void insertTestData() {
    String sql = "insert into producttable (name, description, price, stock_available) values (?, ?, ?, ?);";

    dbHandler.getWritableDatabase();
    database.beginTransaction();
    SQLiteStatement stmt = database.compileStatement(sql);

    for (int i = 0; i < NUMBER_OF_ROWS; i++) {
        //generate some values

        stmt.bindString(1, randomName);
        stmt.bindString(2, randomDescription);
        stmt.bindDouble(3, randomPrice);
        stmt.bindLong(4, randomNumber);

        long entryID = stmt.executeInsert();
        stmt.clearBindings();
    }

    database.setTransactionSuccessful();
    database.endTransaction();

    dbHandler.close();
}

這是使用db的compile sql方式(加速處理,不需要每次都sql command parse)然後綁參數的強力技巧。但是因為這方法需要照欄位的資料型態綁參數,加上insert所指定的欄位,每一個欄位都必需要有資料,其實有點麻煩。不過...

之前寫過一篇關於db架構的文。
用這篇提到的方法架構的db,稍微小改寫一下,要取得欄位名稱跟欄位的資料型態是輕而易舉。
改寫成此法之後... 降為6秒。 應該沒招了。到此為止。



備註:

  • 經由這次教訓之後,決心以後設計xml格式的時候,帶的參數一律用attributes帶。
  • 日期建議直接以字串存取。在不考慮時區的處理,yyyy-MM-dd HH:mm:ss格式是最好用的。要比對最小單位為秒的時間也很簡單,直接使用大小於比對就好。
    轉成Calendar / date等等日期格式再parse出來存將會拖累速度。反正android的sqlite,所有資料都是string存放(連數字都一樣)...