Android效能最佳化之解密ZAKER,網易雲閱讀等新聞應用的內容緩衝載入方式,androidzaker
我是比較關注時事的, 每天都會花一時間點事件去看看新聞什麼的. 因此類似ZAKER, 網易雲閱讀等這類的資訊彙總類應用是我的鐘愛, 並且這些應用也確實做得很好,值得學習! 前面一篇文章, 講了緩衝的一些構思. 之前也寫過LRUCache類的一些緩衝實踐, 但那隻是放在應用的緩衝中,並不適合做長期的緩衝. 這次我們來實踐完整的例子, 模仿ZAKER那類應用是如何載入一條新聞的,並且如何緩衝這條新聞的.
寫在前面, 本篇文章只是其中一種實現方式, 僅為闡述思路, 並不代表最優的做法. 條條大路通羅馬嘛.
實現效果
( 如果打不開, 請複製圖片地址到地址欄開啟. 源碼在文章底部. )
先說效果, 實際使用中, 當使用者第一次開啟一條新聞, 這條新聞出來的是文字, 然後顯示預設載入中的圖片, 使用者慢慢的滑動螢幕瀏覽新聞, 同時新聞中的圖片也慢慢的載入出來, 做的更人性化的, 甚至會將載入中的圖片都顯示百分比或者進度資訊, 讓使用者知道這張圖片大概還差多少就載入完畢. 然後退出當前的頁面, 點擊別的標題, 繼續看別的新聞... 一直重複這個操作.
當因為某些原因, 網路關閉了, 但使用者無聊, 在公交上想看看之前瀏覽過的新聞, 但為了省流量關閉了行動電話通訊而且公交上沒有WIFI. 使用者開啟應用, 點開之前看過的新聞標題, 進入依然能看到之前的完整的新聞, 圖片文字什麼都有.
到了這裡, 我們應該明白, 使用者看過的新聞文字和圖片都緩衝在本地了.上篇文章寫過, 圖片一般適合通過File緩衝在本地, 大篇的文字適合放在資料庫. 沒錯, 就這麼實現~
接下來希望將具體的思路說清楚, 那麼自己實現起來,或者看下面的範例程式碼, 都能有更好的理解.
具體的思路1, 如何?
ZAKER的新聞顯示, 是使用WebView做載體, 然後使用自訂的HTML模版, 將伺服器傳遞到用戶端的資料填充本地的HTML模版去顯示. 好看的HTML模版, 包含不少通用的CSS, JS, 是一個優秀前端開發人員的產品, 作為移動開發, 我們並不需要在這方面瞭解太多. 如果有需要, 大可叫公司的前端開發人員提供對應的模板. 所以我們下面的例子, 只是簡單的HTML模板, 方便闡述原理.
1) 在伺服器擷取用於填充HTML模板的一條新聞內容的資料後, 我們還應當得到這條新聞所包含的圖片資訊, 比片名字, 圖片id, 圖片的下載連結等等... 將文字內容存入資料庫.
2) 然後通過一些方法( 一般是JavaScript ) 在WebView的頁面控制 先顯示文字,和載入中的預設圖片. 同時在本地建立線程去下載這些圖片.
3) 每有一張圖片下載完成, 就將圖片存入本機存放區空間中. 並且將圖片的資訊放入資料庫或其他地方. 這樣以後要用這張圖片, 直接用圖片的下載連結作為查詢條件, 先去資料庫尋找, 如果存在, 就擷取該圖片下載連結所對應的本機存放區路徑資訊. 如果路徑有效則使用, 無效或尋找無果, 則重新下載.
4) 上面下載完一張圖片後還沒結束, 最後, 每下載完一張圖片, 還要在WebView中將預設的圖片替換成所下載的圖片. 整個過程大致如此.
2, 需要一點別的知識 - JavaScript
看了上面四個步驟, 相信在從伺服器擷取資料, 對資料庫SQLite進行存取資料, WebView載入HTML源碼等等這些是毫無難度的. 但有一些地方,確實本地代碼很難完成的, 需要藉助JavaScript. WebView是支援JavaScript的,因為它能完成很多頁面的工作, 比如監聽WebView的網頁滾動座標, 控制頁面滾動等常見的情境, 類似網頁開發的 lazyload( 消極式載入圖片, 非同步載入圖片等 ) 效果都藉助了js實現. 而且JavaScript還能和本地代碼進行互動被調用, 使得WebView變得異常強大. 比如上面的圖片非同步載入( 先顯示預設圖片, 然後圖片下載完去替換預設圖片 ), 在頁面擷取圖片資訊, 調用本地代碼去下載圖片等, 這些都是實現的關鍵地方, 依靠JavaScript完成.
但是要注意, JavaScript是容易產生安全性漏洞的地方, 稍有不慎,就會成為被攻擊的入口. 所以不建議隨意去用.
說了上面一堆, 應該不難理解我們接下來所要實現的功能了吧.但有幾個地方, 在這裡進行說明:
1, WebView是可以直接載入本地資源的, 比如SD卡, assets, raw等檔案夾裡的都可以.
2, 多線程使用資料庫, 比如SQLite. 必須注意並發同步等問題. 對於不是高並發, 線程數也不多的, 最簡單的可以用關鍵字synchronized去解決. 不然很容易拋錯.
3, 圖片儲存和讀取,要注意檔案的體積大小. 具體應用中,推薦的方案是伺服器已經根據移動端對圖片進行了壓縮最佳化再傳遞, 這樣用戶端可以降低處理的複雜度.
設想一下, 有限大小的螢幕中, 一堆文字不佔什麼空間流量, 但卻給了幾張原圖大小的圖片過來, 是毫無意義的. 內容中顯示小圖, 如果使用者點擊圖片彈出, 或者 點擊儲存原圖, 這時才另外下載原圖, 既保證互動的舒適感, 也保證流量和效能都沒有被浪費.
So 直接上代碼, 結合代碼的注釋和上面的說明, 就容易理解多了.
先是用到的2個頁面的簡單XML,
activity_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/container" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.alextam.webviewdemo.MainActivity" tools:ignore="MergeRootFrame" > <TextView android:layout_width="match_parent" android:layout_height="50dp" android:background="#88888888" android:text="測試WebView緩衝" android:textColor="#FFFFFFFF" android:textSize="18sp" android:gravity="center" android:layout_alignParentTop="true" /> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="20dp" android:text="開啟頁面" android:layout_centerInParent="true" /> </RelativeLayout>
webview_act_main.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <WebView android:id="@+id/wv_main" android:layout_width="match_parent" android:layout_height="match_parent" /></RelativeLayout>
入口Activity, MainActivity :
這個類很簡單, 就是建立本機快取的檔案夾用於存放新聞中的圖片.
/** * Created on 5/21/2015 * @author Alex Tam * */public class MainActivity extends Activity {private Button btn_start;private String rootPath;public static final String SEPERATOR = "/";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);init();}private void init(){btn_start = (Button)findViewById(R.id.btn_start);createCacheFolder();btn_start.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {if(createCacheFolder())//先檢測是否成功建立本機快取檔案夾{Intent goIntent = new Intent(MainActivity.this,WebViewActicvity.class);startActivity(goIntent);}}});}//建立本機快取檔案夾 - 存放圖片private boolean createCacheFolder(){if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())){rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + SEPERATOR + "WebViewDemo";File cFile = new File(rootPath);if(!cFile.exists()){cFile.mkdir();}return true;}else{t(this, "無法建立本地檔案夾,請插入SD卡");return false;}}public static final void t(Context context , String c){Toast.makeText(context, c, Toast.LENGTH_SHORT).show();}}
具體顯示效果的Activity, WebViewActicvity:
public class WebViewActicvity extends Activity{private WebView wv_main;//MAP - 存放要顯示的圖片資訊private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();//圖片檔案夾private String rootPath = Environment.getExternalStorageDirectory().getAbsolutePath() + MainActivity.SEPERATOR + "WebViewDemo";private DAOHelper helper;//存放圖片下載器資訊private List<String> taskArray = new ArrayList<String>();@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.webview_act_main);//資料庫操作類helper = new DAOHelper(WebViewActicvity.this);start();}private void start(){wv_main = (WebView)findViewById(R.id.wv_main);wv_main.getSettings().setJavaScriptEnabled(true);wv_main.setWebViewClient(new WebViewClient());// 單列顯示wv_main.getSettings().setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);// 添加JavaScript介面// "mylistner" 這個名字不能寫錯,// 因為在WebView載入的HTML中的JavaScript方法會回調mylistner裡面的方法,所以兩者名字要一致wv_main.addJavascriptInterface(new JavascriptInterface(WebViewActicvity.this), "mylistner");// 為了類比向伺服器請求資料,載入HTML, 我已提前寫好一份,放在本地直接載入wv_main.loadUrl("file:///android_asset/wv_content.html");}private class JavascriptInterface {private Context context;public JavascriptInterface(Context context) {this.context = context;}//該方法被回調替換頁面中的預設圖片@android.webkit.JavascriptInterfacepublic String replaceimg(String imgPosition , String imgUrl, String imgTagId){if(!map.containsKey(imgUrl)){//如果中介儲存空間MAP中存在該圖片資訊,就直接使用,不再去資料庫查詢String imgPath = helper.find(imgUrl);if(imgPath != null && new File(imgPath).exists()){map.put(imgUrl, imgPath);return imgPath;}else{if(taskArray.indexOf(imgUrl) < 0){// 當圖片連結不存在資料庫中,同時也沒有正在下載該連結的任務時, 就添加新的下載任務// 下載任務完成會自動替換taskArray.add(imgUrl);DownLoadTask task = new DownLoadTask(imgTagId, imgPosition, imgUrl);task.execute();}// 為了類比預設圖片的載入進度, 在這裡返回另一張不一樣的預設圖片,// 具體應用中,可以根據需求將該處改為某些百分比之類的圖片return "file:///android_asset/test.jpg";}}else{return map.get(imgUrl);}}}//圖片下載器private class DownLoadTask extends AsyncTask<Void, Void, String>{String imageId; //標籤idString imagePosition;//圖片數組位置標記String imgUrl;//圖片網路連結public DownLoadTask(String imageId, String imagePosition, String imgUrl){this.imageId = imageId;this.imagePosition = imagePosition;this.imgUrl = imgUrl;}@Overrideprotected String doInBackground(Void... params) {try {// 下載圖片URL url = new URL(imgUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setConnectTimeout(20 * 1000); conn.setReadTimeout(20 * 1000); conn.setRequestMethod("GET"); conn.connect(); InputStream in = conn.getInputStream(); byte[] myByte = readStream(in); //壓縮儲存,有需要可以將bitmap放入別的緩衝中,另作他用, 比如點擊圖片放大等等 Bitmap bitmap = BitmapFactory.decodeByteArray(myByte, 0, myByte.length); String fileName = Long.toString(System.currentTimeMillis()) + ".jpg"; File imgFile = new File(rootPath + MainActivity.SEPERATOR +fileName); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(imgFile)); bitmap.compress(Bitmap.CompressFormat.JPEG, 80, bos); bos.flush(); bos.close(); return imgFile.getAbsolutePath(); } catch (Exception e) {e.printStackTrace();}return null;}@Overrideprotected void onPostExecute(String imgPath){super.onPostExecute(imgPath);if(imgPath != null){//對頁面調用js方法, 將預設圖片替換成下載後的圖片String url = "javascript:(function(){" + "var img = document.getElementById(\""+ imageId+ "\");"+ "if(img !== null){"+ "img.src = \""+ imgPath+ "\"; }"+ "})()";wv_main.loadUrl(url);// 將將圖片資訊緩衝進中介儲存空間map.put(imgUrl, imgPath);// 將圖片資訊緩衝進資料庫helper.save(imgUrl, imgPath);}else{Log.e("WebViewActicvity error", "DownLoadTask has a invalid imgPath...");}}}private byte[] readStream(InputStream inStream) throws Exception{ ByteArrayOutputStream outStream = new ByteArrayOutputStream(); byte[] buffer = new byte[2048]; int len = 0; while( (len=inStream.read(buffer)) != -1){ outStream.write(buffer, 0, len); } outStream.close(); inStream.close(); return outStream.toByteArray(); }}
裡面用到的assets檔案夾中的圖片,可以用自己的圖片替換. 關鍵的HTML模板內容,在這裡要先感謝一位前端開發的大神, 我是在他基礎上做了修改. 這是他的原文JavaScript實現 頁面滾動圖片載入 , 推薦讀者先去看看原文, 理解裡面的實現原理, 不懂得地方積極請搜尋或者看書. 裡面用到的DOM方法都是很有用的, 值得鞏固學習!
下面是我修改後的HTML源碼, wv_content.html 放在assets檔案夾 :
( HTML中涉及的新聞文字和圖片 均來自喜歡的 ifanr - 愛範兒網. 去星巴克,喝杯咖啡配科技 個人經常關注的科技新聞網之一, 感謝! )
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>WebViewDemo - Alex Tam</title> <style> body{text-align:center;}.list{margin-bottom:40px;}</style> </head> <body> <div> <div> <h1>去星巴克,喝杯咖啡配科技</h1> <span style="color:purple;">商業 | 陳 昊 | 3 小時前 (轉載自ifanr愛範兒)</span><br /><br /> <div id="content"> 星巴克絕對是全球科技媒體中亮相頻率最高的咖啡店。<br/><br/>他們積極地在科技領域尋求合作,也是全球罕見地設定有首席數位官(Chief Digital Officer)一職的咖啡連鎖企業,擔任此職位的 Adam Brotman 在接受福布斯採訪時曾經表示:“在連接線上和線下的時候,我們認識到互連網的線上資源能夠讓我們能更好地通過全新的互動,大規模地和客戶建立可靠的聯絡。”<br/><br/>一傢具有科技範兒的咖啡館是怎麼樣的?<br/><br/><div class="list"><img class="scrollLoading" id="img_1" xSrc="http://images.ifanr.cn/wp-content/uploads/2015/05/powermat-hands-on.jpg" src="file:///android_asset/nopic.gif"" style="background:url(file:///android_asset/nopic.gif") no-repeat center;" /><br /><h3>Powermat 無線充電<h3></div>走進星巴克還帶著自己的充電器和資料線到處找插座的日子很快就要到頭了。在美國舊金山灣區的星巴克門店已經配備了 Powermat 無線充電系統。<br/><br/>只要將 Powermat 提供的充電環接到手機充電口上,再按照指示將充電環擺放到案頭上的特定位置,位於桌子下方的充電組件就會開始為手機進行無線充電。<br/><br/>早在 2012 年,星巴克就宣布支援 Powermat 背後的無線供電標準聯盟 Power Matter Alliance (PMA),助其推廣該無線充電技術。<br/><br/>而他們的對手 WPA 也非常強勁, 因為 WPA 是 Qi 無線充電標準的推動者——包括三星、LG、諾基亞等大廠商都支援這個標準。<br/><br/>其實這也不是星巴克第一次在標準大戰中站隊了,早在 2001 年,星巴克就已經在科技界發揮了自己的力量,它和蘋果紛紛站在的 Wi-Fi 標準這邊,助其擊敗 Home RF,成為當今家喻戶曉的無線網路標準。<br/><br/><br/><div class="list"><img class="scrollLoading" id="img_2" xSrc="http://images.ifanr.cn/wp-content/uploads/2015/05/starbucks_spotify.jpg" src="file:///android_asset/nopic.gif"" style="background:url(file:///android_asset/nopic.gif) no-repeat center;" /><br /><h3>Spotify 線上音樂<h3></div><br/><br/><br/><div class="list"><img class="scrollLoading" id="img_3" xSrc="http://images.ifanr.cn/wp-content/uploads/2015/05/starbucks-app.jpg" src="file:///android_asset/nopic.gif" style="background:url(file:///android_asset/nopic.gif) no-repeat center;" /><br /><h3>強大的星巴克官方 App<h3><div class="list"><img class="scrollLoading" id="img_4" xSrc="http://images.ifanr.cn/wp-content/uploads/2015/05/starbucks_order_and_pay.jpg" src="file:///android_asset/nopic.gif" style="background:url(file:///android_asset/nopic.gif) no-repeat center;" /><br /><br/><div class="list"><img class="scrollLoading" id="img_5" xSrc="http://images.ifanr.cn/wp-content/uploads/2015/05/starbucks-postmates.jpg" src="file:///android_asset/nopic.gif" style="background:url(file:///android_asset/nopic.gif) no-repeat center;" /><br /><br/></div><h5>星巴克的官方 App 還整合了移動支付功能,而且非常受歡迎。根據星巴克2015 年第一財季的財報,在美國市場,他們平均每周會有 700 萬筆交易通過手機完成,占交易總數 16%。</h5><br/><br/>一個咖啡連鎖店的 App,居然變成了全美最受歡迎的移動支付應用之一。<br/><br/><br/><a href="http://www.ifanr.com/522993">原文連結</a><br/><br/> </div> </div> </div> <script type="text/javascript"> var scrollLoad = (function (options) { var defaults = (arguments.length == 0) ? { src: 'xSrc', time: 1000} : { src: options.src || 'xSrc', time: options.time ||1000}; var camelize = function (s) { return s.replace(/-(\w)/g, function (strMatch, p1) { return p1.toUpperCase(); }); }; this.getStyle = function (element, property) { if (arguments.length != 2) return false; var value = element.style[camelize(property)]; if (!value) { if (document.defaultView && document.defaultView.getComputedStyle) { var css = document.defaultView.getComputedStyle(element, null); value = css ? css.getPropertyValue(property) : null; } else if (element.currentStyle) { value = element.currentStyle[camelize(property)]; } } return value == 'auto' ? '' : value; }; var _init = function () { var offsetPage = window.pageYOffset ? window.pageYOffset : window.document.documentElement.scrollTop, offsetWindow = offsetPage + Number(window.innerHeight ? window.innerHeight : document.documentElement.clientHeight), docImg = document.images, _len = docImg.length; if (!_len) return false; for (var i = 0; i < _len; i++) { var attrSrc = docImg[i].getAttribute(defaults.src), o = docImg[i], tag = o.nodeName.toLowerCase(), imgId = o.id;; if (o) { postPage = o.getBoundingClientRect().top + window.document.documentElement.scrollTop + window.document.body.scrollTop; postWindow = postPage + Number(this.getStyle(o, 'height').replace('px', '')); if ((postPage > offsetPage && postPage < offsetWindow) || (postWindow > offsetPage && postWindow < offsetWindow)) { if (tag === "img" && attrSrc !== null) { o.src = window.mylistner.replaceimg(i,attrSrc,imgId); } o = null; } } }; window.onscroll = function () { setTimeout(function () { _init(); }, defaults.time); } }; return _init(); }); scrollLoad();</script> </body> </html>
該樣本實現, 當進入WebView的頁面是, 首次載入HTML會顯示文字和預設的圖片, 然後顯示另一張載入中的圖片(比如代表進度), 同時需要網路下載圖片, 當圖片下載完成, 無論頁面滾動不滾動都會替換舊的圖片. 這時圖片的資訊會同時放入資料庫和中介儲存MAP中, 如果MAP存在圖片資訊就不去資料庫尋找了(能在應用內擷取的資料, 盡量不去資料庫讀取. ) 當關閉頁面, 再次進入, 即使關閉網路, 也能很快的顯示原圖. 整個效果就是這樣吧.
另外, 上面除了本地代碼, HTML中涉及的JavaScript代碼部分也是實現的關鍵. 該 js 是每次滾動頁面都會去檢測img標籤的資訊, 這樣每次滾動都有了檢測圖片載入進度的機會. 同時將圖片替換的js 介面放在本地去載入調用. 結合兩者, 其實可以增加實現某張圖片的載入進度百分比, 當然每個百分比其實是對應各自的圖片, 只需要在不同進度的時候返回代表某個百分比的圖片即可. 例子中, 將緩衝實現的重點放在了圖片, 重在闡釋這次模仿ZAKER這類應用緩衝新聞的實現原理, 所以如何將文字儲存在資料庫就沒實現了, 相信大家都能自己做的哈. 希望這次的例子, 能協助加深我們大家對新聞應用內容載入方式的理解, 從而做出更好的應用.
就寫到這了, 晚安.
源碼下載