記,基於Android開發類似於微博的東東時,值得記錄的幾個問題~,android類似於

來源:互聯網
上載者:User

記,基於Android開發類似於微博的東東時,值得記錄的幾個問題~,android類似於

作為一個Java的使用者,在經曆了Web到服務端開發的工作後,今年終於開始接觸一些android開發方面的工作了。

新的挑戰~~最近有一個需求是在應用裡開發一個類似於微博的功能模組,說難不難,說易不易~~

作為一名Android上的菜鳥,在開發的過程裡還是遇到不少問題的。當然,緊接著的就是一個個的想辦法解決問題~~~~~

一直想把過程中遇到的,自己覺得幾個比較有意義的問題,及其解決方案記錄下來,但苦逼的是最近一直沒有多的時間~~~

今天又到了一周一度的美好周末,陽光明媚,那乾脆起個早,來寫一寫,一來也給自己加深下印象~~~

另外,如果您也是一個剛剛開始接觸Android的菜鳥,希望能給您帶去一點協助。

而同時,如果您看到其中的某處應用不當,或者有更好的實現方式,更希望您能不吝指出,協助我進步~


問題剖析:


開發類似於微博的這種功能,首先想到的,自然就是會用到ListView。那麼,這其中會遇到的幾個問題在什麼地方呢?


1、首先,與普通的ListView定義不同,像微博這種東西,內容存在“不確定性”。這個不確定性是指什麼呢?比如,有的微博內容裡可能會帶有圖片,而有的則可能為純文字;而在帶有圖片的微博中,圖片的數目也是不確定的。所以說,對於介面的定義,自然就不能再僅僅依靠布局檔案了。而需要藉助代碼在類檔案中實現“動態載入控制項”。


2、第二個問題,也是很常見的問題,就是在該種介面中,通常會包含大量的圖片,例如帳戶圖片,微博內容裡的圖片等等。這個時候自然就需要新開線程去處理從伺服器下載圖片,並更新介面的操作。也就是所謂的“圖片的非同步載入”工作。


3、與之伴隨而來的,就是關於圖片載入的另一個問題,介面裡的圖片很多。如果每次載入時,我們都要從伺服器去下載,首先的問題就是載入的速度;其次這樣的實現方式,對於網路資源的使用,只能說“抵制鋪張浪費,從我做起”。那麼,對應的,就需要實現“圖片的緩衝”。


4、最後一個想要記錄的問題,是比較有意思的問題,也是過程中讓我最蛋疼的問題。那就是Android對於ListView控制項的“Recycler”機制,導致圖片會出現顯示錯亂的問題。


針對於這些問題,從床上爬起來理一理思路,重寫了一個Demo,大體效果如下:

       

接著,我們就按照開發這個玩意兒的步驟走一遍,然後看針對於上面提出的幾點值得注意的問題,其解決之道是什嗎?


一、布局檔案的定義

正如同建築師們建造一幢精美的建築,得先畫出設計圖紙一樣。我們既然要開發一個我們自己的“微博”,那我們就先搞出“微博”介面的布局檔案。


但針對於這一點並沒有太多值得額外提到的地方,只需要按照自己想要的樣式來定義自己的布局檔案就行了。



二、類的定義

當我們已經有了“設計圖”,接下來就是實際的“建築工作”了。


首先,我們會定義一個繼承於Activity的類來關聯我們定義的布局檔案。

接著,因為我們所定義的微博內容的介面中,使用了ListView控制項。而ListView控制項的具體內容,則需要由一個Adapter來提供。所以我們還需要定義一個Adpater類。

這時候,我們上面談到的第一個問題就來了:“內容的不確定性”。基於存在有的微博可能為純文字,有的帶有圖片;帶有圖片的微博中,有的僅僅只有一張圖片,有的可能兩張,也有可能更多的這種情況。

那麼,針對於圖片的顯示,我們就應該在代碼中進行動態添加對應數目的“ImageView”。


所以,在我們定義的Adpater中的getView方法中,可能會存在類似於這樣的代碼:

BlogInfo info = blogsDownLoad.get(position);if (convertView == null) {// init item viewconvertView = mInflater.inflate(R.layout.micro_blog_item, null);holder = initViewHolder(convertView);// 如果該條微博還帶有圖片if (info.getImages() != null && !info.getImages().equals("")) {String[] imageArray = info.getImages().split(";");// 動態載入圖片顯示控制項ImageView imageView = new ImageView(context);imageView.setLayoutParams(new ViewGroup.LayoutParams(250, 250));holder.images_layout.addView(imageView);//.....}convertView.setTag(holder);}

現在,簡單的來說,我們已經初步解決了關於“動態載入控制項的”問題。

而當我們已經定義好了顯示微博內容的Adpater之後。我們馬上將要面臨的就是上面談到的下一個問題:“圖片的非同步載入”。

那麼,首先我們需要明確的就是,為什麼我們要對圖片做非同步載入?這是因為:

在Android當中,當一個應用程式的組件啟動的時候,並且沒有其他的應用程式組件在運行時,Android系統就會為該應用程式組件開闢一個新的線程來執行。

預設的情況下,在一個相同Android應用程式當中,其裡面的組件都是運行在同一個線程裡面的,這個線程我們稱之為Main線程。

當我們通過某個組件來啟動另一個組件的時候,這個時候預設都是在同一個線程當中完成的。當然,我們可以自己來管理我們的Android應用的線程,我們可以根據我們自己的需要來給應用程式建立額外的線程。

也就是說,在Android中,對於“應用介面”的管理,都是在主線程當中完成的。所以,永遠不要在主線程中做耗時的操作!

在我們這裡所說的“微博”來講,從伺服器去下載圖片到我們的用戶端應用進行顯示,這就是一個所謂的耗時操作。更何況,我們下載的圖片的數量可能還很大。

那麼,如果我們不對其進行“非同步下載”的處理,會帶來的影響就例如:

直到我們介面上所需要顯示的所有圖片下載完成之前,主線程一直都處於一個“阻塞”的狀態。

而這反應在使用者體驗上,也就是應用一直處於頓卡狀態,無法響應使用者其它任何的新的操作。

更糟糕的是,當我們的整個現場如果阻塞時間超過5秒鐘(官方是這樣說的),這個時候就會出現 ANR (Application Not Responding)的現象,此時,應用程式會彈出一個框,讓使用者選擇是否退出該程式。這當然是糟糕透了的情況。


所以,我們自然會選擇對“下載圖片”的操作進行“非同步實現”。這聽上去很高大上的術語,其實原理很簡單。

既然不要在主線程當中做耗時的操作,那我們要做的既然就是新開一個輔助線程,到伺服器下載圖片,當圖片下載完成後,再通知主線程更新介面的顯示。

Android提供了兩種方式來解決線程直接的通訊問題,一種是Handler機制,另一種就是AsyncTask機制。


我們這裡選擇使用AsyncTask機制,來實現所謂的“圖片的非同步載入”:

public class AsynImageLoader extends AsyncTask<String, Integer, Bitmap> {private String imageUrl;private ImageView imageView;public AsynImageLoader(ImageView imageView) {this.imageView = imageView;}@Overrideprotected Bitmap doInBackground(String... params) {Bitmap bitmap = null;try {imageUrl = params[0];URL url = new URL(imageUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setConnectTimeout(5000);conn.setRequestMethod("GET");if (conn.getResponseCode() == 200) {InputStream inputStream = conn.getInputStream();bitmap = BitmapFactory.decodeStream(inputStream);}} catch (IOException e) {e.printStackTrace();}return bitmap;}@Overrideprotected void onPostExecute(Bitmap result) {super.onPostExecute(result);if (result != null) {// 通過 tag 來防止圖片錯位if (imageView.getTag() != null&& imageView.getTag().equals(imageUrl)) {imageView.setImageBitmap(result);}}}}
這個類的思路很簡單,在該類的建構函式中,我們擷取兩個參數:

一個是要進行非同步載入的圖片的URL,我們通過這個URL進行網路下載。

另一個則是在應用中,要將這張載入的圖片顯示到程式介面上的ImageView控制項。

接著,我們在doInBackground方法中,下載這張圖片。當圖片下載完成後,onPostExecute收到通知,將下載到的圖片載入到對應的控制項上去。

也就完成了,我們所謂的“圖片的非同步載入”的工作。


此時,我們已經對圖片添加了“非同步載入”的處理方式。這很不錯,但這顯然還遠遠不夠,因為我們還需要解決我們上面談到的第三個問題:“浪費可恥”!

之所以這樣講,是因為,此時我們對於擷取圖片的方式仍然只有一種,就是“從網路下載擷取”。這樣做的結果就是,我們上次下載好的圖片,絲毫不具備重用性。

例如:我們此次瀏覽了一些內容後,退出了應用;又或者我們在不斷上下滑動,或重新整理著螢幕,基於Android中ListView自身的特點,都需要一次次的去重複下載圖片。

這時,我們要做的,就是添加“緩衝機制”,當我們從網路中下載好圖片之後,就將下載好的圖片存放到緩衝當中去,當下次需要使用到某張圖片資源的時候,我們先到緩衝中去查看是否存在,如果存在則直接擷取,如果不存在,才到網路上去下載。

這樣做的好處很明顯,一直為使用者節省了“網路資源”,另外也很大程度上的提高了擷取資源的速度。這是顯而易見的,你家裡有一個儲物室,當你需要一件物品,先看看家裡的儲物室裡有沒有,有則直接拿來使用,沒有的話,再驅車去外面的商場購買;和每次一有需求,則開著車跑到老遠的地方購買,這其中節約的時間,不言而喻。


廢話不說,Android中對於圖片的記憶體緩衝,最常使用到的是LruCache。所以,我們進一步改進程式,將“緩衝”與“非同步”結合起來,所以我們的圖片載入工具類,可能變成了下面這樣:

@SuppressLint("NewApi")public class AsyncImageLoader {// 圖片緩衝private LruCache<String, Bitmap> mMemoryCache;//private static AsyncImageLoader instance = null;private AsyncImageLoader() {// 擷取到可用記憶體的最大值,使用記憶體超出這個值會引起OutOfMemory異常。// LruCache通過建構函式傳入緩衝值,以KB為單位。int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);// 使用最大可用記憶體值的1/8作為緩衝的大小。int cacheSize = maxMemory / 8;mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap bitmap) {// 重寫此方法來衡量每張圖片的大小,預設返回圖片數量。return bitmap.getByteCount() / 1024;}};}public static AsyncImageLoader getInstance() {if (instance == null) {instance = new AsyncImageLoader();}return instance;}private void addBitmapToMemoryCache(String key, Bitmap bitmap) {Log.v("jiaqi,jiaqi", "ggogo");if (getBitmapFromMemCache(key) == null) {mMemoryCache.put(key, bitmap);}}private Bitmap getBitmapFromMemCache(String key) {return mMemoryCache.get(key);}public void displayImage(String imgUrl, ImageView imageView) {final Bitmap bitmap = getBitmapFromMemCache(imgUrl);if (bitmap != null) {Log.v("記憶體有了", "直接擷取");imageView.setImageBitmap(bitmap);} else {Log.v("記憶體沒得", "去網上下");AsyncImageTask task = new AsyncImageTask(imageView);task.execute(imgUrl);}}//class AsyncImageTask extends AsyncTask<String, Integer, Bitmap> {private String imageUrl;private ImageView imageView;public AsyncImageTask(ImageView imageView) {this.imageView = imageView;}@Overrideprotected Bitmap doInBackground(String... params) {Bitmap bitmap = null;try {imageUrl = params[0];URL url = new URL(imageUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();conn.setConnectTimeout(5000);conn.setRequestMethod("GET");if (conn.getResponseCode() == 200) {InputStream inputStream = conn.getInputStream();bitmap = BitmapFactory.decodeStream(inputStream);}} catch (IOException e) {e.printStackTrace();}return bitmap;}@Overrideprotected void onPostExecute(Bitmap result) {if (result != null) {// 通過 tag 來防止圖片錯位if (imageView.getTag() != null&& imageView.getTag().equals(imageUrl)) {imageView.setImageBitmap(result);}addBitmapToMemoryCache(imageUrl, result);}}}}
這個類的實現,正如我們上面所講的一樣,我們首先在記憶體中開闢一片地區作為圖片資源的緩衝,每次載入一張圖片時,我們都先看看緩衝中是否已經有這張圖片了,如果沒有,我們才會去通過網路進行下載。

當然,這裡為了偷懶和僅僅出於一個說明作用,僅僅只是簡單的使用了記憶體緩衝。實際開發中,更為科學的來講,你還可以選擇使用“多級緩衝”,例如你還可以在本地檔案中開闢緩衝,實現:首先到記憶體緩衝中尋找,如果沒有,則到本地檔案中尋找,如果還沒有,再到網路上去下載。這樣,就更為合理了。


當然,要十分優秀的實現這樣的需求,需要花費不少的精力。所以也可以選擇使用一些圖片載入架構,例如:Android-Universal-Image-Loader。這些優秀的架構已經幫你實現了各種關於圖片處理的需求,你只需要匯入一個第三方包,然後調用API就搞定了。



走到此時,對於這樣一個類似微博的功能,我們已經實現的算是不錯了。但最讓人蛋疼問題,也就是上述的第4個也是最後一個問題,就出現了。

你可能會發現這樣的情況,本來位於ListView第7行的使用者的頭像,莫名其妙顯示為第1行的使用者的頭像。然後在你上下滑動螢幕,ListView進行重新整理的過程中,你蛋疼的發現:“擦,全尼瑪亂套了”。。

而針對於這樣的問題,只要你耐心,上網多查查資料,就會初步得到一個解決方案,為顯示頭像的ImageView控制項,添加一個Tag,這個tag的值通常就使用的是這個ImageView對應要顯示的圖片的URL。

我最開始,也是這樣解決的。但問題雖然解決了,我其實還是不沒有很明白造成這樣的情況的原因。於是當這個問題解決之後,我發現了一個更操作的問題。

上面我們說過了,“微博”的內容存在“不確定性”。於是,我又發現了這樣的情況,當我點擊載入更多按鈕,擷取到新的微博資訊,然後下拉螢幕的過程中,也許第七條微博是沒有圖片內容的,但它卻莫名其妙的載入出了一個圖片內容,而同時你會發現,這個圖片內容實際上是前面第二條微博的。

好吧,我只能說,我淩亂了。。。於是繼續查資料,功夫不負有心人,終於在一片部落格裡發現了這個現象發生的原因,也就是所謂的“recycler”機制。

具體說明,可以參照這篇部落格:【Android】ListView中getView的原理與解決多輪重複調用的方法


其實看了這明白了這篇部落格之後,就會知道:之所以出現這樣的錯誤情況,是因為我們在getView方法中,選擇使用了一個viewHolder來協助我們對介面中的控制項進行複用。在這種情況下,我們的getView方法的實現通常類似於這樣:

public View getView(int position, View convertView, ViewGroup parent) {// 根據Position分別擷取容器當中存放的每條微博的詳情if(convertView==null){convertView = mInflater.inflate(R.layout.micro_blog_item, null);holder = initViewHolder(convertView);}else{holder = (ViewHolder) convertView.getTag();}// 通過holder擷取item項的各個組件,為其做特定的賦值工作return convertView;}

但是,如果我們不使用viewHolder,而是每次調用getView方法時,都選擇使用最原始的類似於:imageView = (ImageView) convertView.findViewByID(....)這樣的方式的話,其實是不會出現這樣的問題的。

你可能會想,既然這樣,我們還為什麼要使用viewHolder來協助實現呢?原因很簡單,我們前面也說到了,是為了實現複用,從而提高效率。

因為正常情況下,一個ListView中的每個item,也就是每個清單項目,它的控制項構成,其實是一樣的。所以,我們當然不要花費更多的勞力,每次getView時,都去資源裡findViewByID一次。

所以,在這種情況下,使用viewHolder就能很好的協助我們避免這一個問題。但是,因為在我們這裡“內容存在不確定性”的特殊情況下,就導致了上面我們所說的蛋疼的問題。

要理解我這裡說的東西,首先需要弄沒明白上面提到的這邊部落格裡講到的"recycler"機制。當明白這個機制 之後,我們就能對上面我所說的類似的錯誤情況,分析出原因了。

例如,我們第一次進到微博介面時,從伺服器下載了5條微博資訊到用戶端進行顯示,這個時候當程式調用getView方法時,他會判斷為此時每個Item都是空的,都需要重新擷取,所以,它都會走“if(convertView == null)”中的內容,但可能當你載入更多之後,向下滑動螢幕,想要瀏覽第六條或者第七條微博時,出於“recycler”機制,他就會去複用之前的convertView,所以這個時候也許就恰巧複用到了被放入"recycler"當中的原本第一條微博內容的“convertview”,而走到"else"裡的代碼執行。於是這個時候,錯誤的圖片顯示情況就出現了。


但是現在,錯誤已經不可怕了,因為我們已經知道了錯誤出現的原因,知道了原因,我們就能針對其給出解決方案。既然圖片顯示錯誤是因為複用了item內容造成的,那麼,我們就應該在其複用時,額外再做一次判斷。

例如,我們的微博介面中,原本的第一條微博帶有1張圖片內容,當我滑動螢幕到顯示第七條微博時,因為這個時候會複用到第一條微博的convertView,所以原本不含有圖片內容的第七條微博也顯示出了一張圖片。這個時候,我們要做的就是,在 複用Convertview的時候,額外做一個判斷,先擷取第七條微博的內容資訊,判斷其是否帶有圖片,如果不帶有,我們則應該將複用的這個convertView中,用於顯示微博所帶圖片內容的這個imageview控制項去掉。這個時候,就不存在混亂的顯示情況了。


所以,經過修改後的adpater類變為了下面的樣子:

package com.tsr.mymicroblog;import java.util.HashMap;import java.util.List;import java.util.Set;import com.tsr.bean.BlogInfo;import com.tsr.util.AsyncImageLoader;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.Button;import android.widget.ImageView;import android.widget.LinearLayout;import android.widget.TextView;public class MicroBlogAdapter extends BaseAdapter {private Context context;// 存放下載微博的容器private List<BlogInfo> blogsDownLoad;private LayoutInflater mInflater;private ViewHolder holder;public MicroBlogAdapter(Context context, List<BlogInfo> blogsDownLoad) {this.context = context;this.blogsDownLoad = blogsDownLoad;this.mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);}@Overridepublic int getCount() {return blogsDownLoad.size();}@Overridepublic Object getItem(int arg0) {// TODO 自動產生的方法存根return null;}@Overridepublic long getItemId(int arg0) {// TODO 自動產生的方法存根return 0;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {// 根據Position分別擷取容器當中存放的每條微博的詳情final BlogInfo info = blogsDownLoad.get(position);if (convertView == null) {// init item viewconvertView = mInflater.inflate(R.layout.micro_blog_item, null);holder = initViewHolder(convertView);// 如果該條微博還帶有圖片if (info.getImages() != null && !info.getImages().equals("")) {String[] imageArray = info.getImages().split(";");// 動態載入圖片顯示控制項fillBlogImageDynamic(holder, imageArray);}convertView.setTag(holder);} else {holder = (ViewHolder) convertView.getTag();// 清除ListView的ReCycle機制當中的ImageView,避免圖片顯示錯亂的情況if (holder.blog_detail_image != null&& holder.blog_detail_image.size() != 0) {cleanOldBlogImages(holder);}// 顯示新的圖片內容if (info.getImages() != null && !info.getImages().equals("")) {// 添加該條微博對應圖片數量的的ImageViewString[] imageArray = info.getImages().split(";");fillBlogImageDynamic(holder, imageArray);}}holder.user_nickname.setText(info.getUsername());holder.publish_time.setText(info.getTime());holder.blog_content.setText(info.getBlogtext());holder.btn_review.setText(context.getString(R.string.blog_review) + "("+ info.getReviewcount() + ")");holder.btn_nice.setText(context.getString(R.string.blog_nice) + "("+ info.getDianzancount() + ")");String headImgURL = MicroBlogActivity.USER_HEAD[position];holder.user_head.setTag(headImgURL);AsyncImageLoader.getInstance().displayImage(headImgURL,holder.user_head);// 根據不同情況,動態設定微博詳情內的圖片內容Set<String> keySet = holder.blog_detail_image.keySet();for (String key : keySet) {String imageName = key;ImageView imageView = holder.blog_detail_image.get(key);imageView.setTag(imageName);AsyncImageLoader.getInstance().displayImage(imageName, imageView);}// 點贊按鈕holder.btn_nice.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// do something there...}});// 舉報按鈕holder.btn_report.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {// do something there...}});return convertView;}static class ViewHolder {// 相關介面組件private ImageView user_head;private TextView user_nickname;private TextView publish_time;private TextView blog_content;private ImageView blog_pics;private Button btn_report;private Button btn_review;private Button btn_nice;private LinearLayout images_layout;private HashMap<String, ImageView> blog_detail_image = new HashMap<String, ImageView>();}public void addItem(BlogInfo blog) {blogsDownLoad.add(blog);}private ViewHolder initViewHolder(View convertView) {holder = new ViewHolder();holder.user_head = (ImageView) convertView.findViewById(R.id.img_wb_item_head);holder.user_nickname = (TextView) convertView.findViewById(R.id.txt_wb_item_uname);holder.publish_time = (TextView) convertView.findViewById(R.id.txt_wb_item_time);holder.blog_content = (TextView) convertView.findViewById(R.id.txt_wb_item_content);holder.btn_report = (Button) convertView.findViewById(R.id.btn_report);holder.btn_review = (Button) convertView.findViewById(R.id.btn_review);holder.btn_nice = (Button) convertView.findViewById(R.id.btn_nice);holder.blog_pics = (ImageView) convertView.findViewById(R.id.img_wb_item_content_pic);holder.images_layout = (LinearLayout) convertView.findViewById(R.id.blog_images);return holder;}private void fillBlogImageDynamic(ViewHolder holder, String[] imageArray) {for (int i = 0; i < imageArray.length; i++) {ImageView imageView = new ImageView(context);imageView.setLayoutParams(new ViewGroup.LayoutParams(250, 250));holder.images_layout.addView(imageView);holder.blog_detail_image.put(imageArray[i], imageView);}}private void cleanOldBlogImages(ViewHolder holder) {HashMap<String, ImageView> imageMap = holder.blog_detail_image;// 刪除原來的ImageViewif (imageMap != null && imageMap.size() > 0) {holder.images_layout.removeAllViews();imageMap = new HashMap<String, ImageView>();}}}



到了這裡,提到的幾個問題也講完了~~~~~




聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.