App競品技術分析 (7)逼出來的奇思妙想
1 一切皆可配置
1.1 使用XML配置首頁,防止因載入不到資料而沒有入口
在很多電商類App中,我們會看到有一個配置檔案或者JSON檔案,裡面存放著首頁展示所需要的所有資料,包括圖片、文字等等,點選後能進入各個品類這些二級頁面,如圖9-15所示,我們可以看到,這個首頁由3個Tab組成:首頁、發現、個人中心,配置檔案中指定了每個Tab的顯示文字、點選後對應的ViewController、所需的預設圖和高亮圖:
圖9-15 某款App首頁的plist配置檔案
這麼做是因為,如果獲取首頁資訊的MobileAPI介面掛了,或者說,就在我們呼叫該介面的時候掛了,那麼首頁仍然能通過讀取這個本地的配置檔案或者JSON檔案而正常顯示,仍然能看到各個品類的入口,點選後進入,這樣不影響生意。
但是這個配置檔案或者JSON檔案可能不是線上最新的資料,所以一種好的解決方案是,第一次啟動App的時候把這個檔案複製到本地,然後每次呼叫首頁MobileAPI介面渠道資料後就把資料同步到這個檔案,這樣就確保了下次如果呼叫MobileAPI介面不通,仍然能顯示比較新的資料。
1.2 配置頁面的公共行為
把首頁的資料配置在XML中只是第一步,這個世界上不乏野心者,他們想把更多公用的東西做成可配置化。
比如說,呼叫MobileAPI時是否要要顯示進度條,進度條中是否有取消按鈕,點選取消按鈕後是後退到上一頁還是停留在當前頁面,呼叫MobileAPI錯誤是否要顯示錯誤提示,如下所示:
<ShowSettingshowLoading="1" showCancel="0" goBackAfterCancel="1" showErrorInfo="1" />
又比如說,進這個頁面,是否要登入,如下所示:
<WindowType needLogin="1" />
所有這些資訊都定義在配置檔案中。我們應該在App中編寫一套頁面引擎,自動讀取配置資訊,這樣就能少寫很多很多程式碼。開發人員就可以把更多精力放在業務邏輯的實現上。
2 App後門
任何成熟App都會為自己留一個後門,目前業界有兩種做法:
一種做法是,只有Debug版本能看到這個後門,而Release版本看不到。
另一種做法是,在線上Release版本中很深的一個頁面,比如設定頁面,點選某個特定的區域很多次後彈出一個對話方塊,要求輸入密碼,輸入正確就能進入這個後門。
留一個後門有很多好處,列舉如下:
1. 做一個能切換伺服器的頁面。這樣就可以在開發期間,從線上環境切換到測試環境而不需要重新打個包,極大的方便了測試團隊對新功能進行驗收。
2. 要測試某個頁面請求了哪些MobileAPI介面,打印出呼叫這些介面時輸入的引數和返回JSON資料。這樣就能夠在線上App發現某個頁面有問題時,及時在App後門中檢查資料是否正常,而不用App開發人員和MobileAPI開發人員坐在一起逐行聯調程式碼,極大的節省了人力。
3. 對於App崩潰,我們將最後一次崩潰的資訊記錄在本地,然後可以通過後門看到這個崩潰資訊。這對於測試期間不經意點出來的崩潰,可以迅速追蹤到問題的所在。當然,另一種方案是把崩潰資訊傳送到伺服器,然後我們去伺服器抓取崩潰資訊,但是這樣不及時;而對於那些發現App崩潰然後來找我嗎的同事朋友來說,通過後門看崩潰日誌是最好的途徑。
4. 提供一個後門頁面供Html5團隊進行除錯,該頁面內建一個WebView,載入Html5團隊正在開發的Html頁面,要支援除錯。
5. 對我們的App進行流量測試,統計某個頁面所花費的流量,包括呼叫MobileAPI、下載圖片、上傳檔案、XMPP聊天等等。其中,從App啟動到首頁載入完成所花費的流量是我們關心的一個關鍵點,而手機待機時,App所花費的流量也是我們所關心的。我們需要這樣一個後門頁面,看到這些資料統計。
6. 對我們的App進行電池電量消耗測試。需要有個後門頁面記錄每次開啟App和退出App的時間,以及這段時間內我們的App所消耗的電量。為了確保資料的準確性,需要確保手機上只安裝了一個App,而且處於相同的網路環境下,比如3G。
前面說到開一個後門,提供切換伺服器的功能。這樣測試人員可以在這個後門頁面靈活配置當前MobileAPI要連線哪個伺服器。
基於此,這個後臺頁面需要顯示伺服器清單列表,而這個列表從App包中的一個檔案讀取,此外,還要支援手動輸入伺服器地址,因為有時候要直接連線到MobileAPI開發人員的機器,把他們的開發機器作為臨時伺服器。
3 Android包中META-INF目錄的妙用
對於Android批量打渠道包,每個團隊都有切身的痛。每個包,經過混淆和簽名,都至少要3分鐘時間,300多個包就是十幾個小時才能全打出來,所以一般在晚上幹這個事情。
一般而言,我們在App每次啟動時從AndroidManifest.xml這個檔案讀取渠道名稱,如下所示,其中360Android是渠道名稱:
然後在App中,每次從AndroidManifest.xml中取出這個渠道名稱,傳遞給友盟或者我們自己的MobileAPI介面,如下所示,演示瞭如何取得渠道名稱的方法:
private String getChannel(Context context) { try { PackageManager pm = context.getPackageManager(); ApplicationInfo appInfo = pm.getApplicationInfo( context.getPackageName(), PackageManager.GET_META_DATA); return appInfo.metaData.getString("channel"); } catch (PackageManager.NameNotFoundException ignored) { } return ""; }
上述是傳統的做法,我們接下來介紹一種更快的做法。
我也是偶然的機會,看到一些知名的App包裡面的META-INF目錄,會有一個0位元組的檔案,檔名是某個渠道的值,於是我就大膽猜測,這個檔案是用來批量打渠道包的。
我上網查了一下這個META-INF目錄的功用,發現修改這個目錄裡面的檔案,是不需要重新簽名App的。於是我們可以這樣做:
- 打包流程上的優化:
打一個簽名混淆過的正式包,我們稱之為“母體”,然後往這個apk包中插入一個名為channel_360Android的空檔案。這樣一個渠道包就完成了,如圖9-16所示:
圖9-16 META-INF目錄下的空檔案
之所以在空檔案的名稱前面加上channel_的字首,是為了在執行期查詢這個檔案的時候,可以快速找到。
準備一個渠道列表檔案channel.txt,檔案內容由3個渠道組成,每個渠道佔一行,如圖9-17所示:
圖9-17channel.txt檔案內容
接下來我們使用Python指令碼build.py,遍歷這個渠道列表檔案,逐個生成渠道包,指令碼如下所示:
import zipfile import shutil import os base_dir = '/Users/Shared/' apk_name = 'ChannelDemo' apk_path = base_dir + apk_name + '.apk' empty_file = base_dir + 'baojianqiang' f = open(empty_file, 'w') f.close() channel_file = base_dir + 'channel.txt' f = open(channel_file) lines = f.readlines() f.close() output_dir = base_dir + 'output' if not os.path.exists(output_dir) os.mkdir(output_dir) for line in lines target_channel = line.strip() target_apk = output_dir + '/ChannelDemo_' + target_channel + '.apk' shutil.copy(apk_path, target_apk) zipped = zipfile.ZipFile(target_apk, 'a', zipfile.ZIP_DEFLATED) empty_channel_file = 'META-INF/channel_' + target_channel zipped.write(empty_file, empty_channel_file) zipped.close()
這樣的話,生成第一個“母體”包需要幾分鐘時間,但是之後生成其他渠道包的時間就快了,就都是在apk中插入一個空檔案的時間了。
- 執行期間的優化:
我們改為從META-INF目錄讀取那個0位元組的檔名稱,從中得到這個apk包的渠道號,網上有很多這樣的程式碼例子,我就不過多介紹了。
按照上述打包新流程,一分鐘打幾百個渠道包不成問題。
4 classes.dex的拆與合
一般而言,Android App中的dex檔案只有一個。但是對於很多業務邏輯複雜的App,當方法數量超過dex的最大限制65535時,編譯就會出錯。業界稱之為“爆棚”。
一種解決方案就是外掛化。把那些獨立的模組做成一個apk,然後在使用的時候,使用DexClassLoader進行載入。對那些暫時還沒有外掛化程式設計的App而言,這種解決方案太遙遠了。
另一種解決方案就是dex分包。就是將classes.dex拆分為2個dex,讓每個dex包的方法數量都小於65535。很多App都是這麼做的,有的App甚至拆分成6個dex之多。
Google提供了dex拆包工具multidex。關於這個工具的使用方法,請參見CSDN上時之沙的部落格文章[1]。
但multidex仍然解決不了開發除錯期間方法數超過dex上限的問題,開發人員不能每次除錯都使用Gradle去打包,於是我們只好把不重要的SDK引用臨時去掉,比如說GA打點,同時註釋掉引用了這個SDK的程式碼,這樣就能正常編譯和除錯了,最後在提交測試或釋出市場打包的時候再把刪除的引用和註釋掉的程式碼恢復過來。
每次都這麼搞可不行,嚴重的擾亂了開發節奏,於是我們採取在程式碼中動態載入dex的方式。對於那些第三方SDK,比如說Umeng,Google Analytics,aSmack,JPush,都是導致dex方法數徒增最終達到上限的“殺手”級SDK,所以我們優先把這些jar包提出來,放到一個apk中,作為第二個dex。這樣我們就能使用DexClassLoader載入這些SDK啦。
只要能把方法數降低65535以下,就又可以在各種IDE中正常開發除錯了。
關於動態載入dex的技術,請參見以下文章,有更加詳盡的介紹:
[1] 博文地址:http://blog.csdn.net/t12x3456/article/details/40837287
[2] 博文地址:http://android-developers.blogspot.hk/2011/07/custom-class-loading-in-dalvik.html
[3] 博文地址:http://tech.meituan.com/mt-android-auto-split-dex.html
[4] 博文地址:http://my.oschina.net/853294317/blog/308583