記,基於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 view
			convertView = 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;
	}

	@Override
	protected 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;
	}

	@Override
	protected 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) {
			@Override
			protected 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;
		}

		@Override
		protected 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;
		}

		@Override
		protected 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);
	}

	@Override
	public int getCount() {
		return blogsDownLoad.size();
	}

	@Override
	public Object getItem(int arg0) {
		// TODO 自動生成的方法存根
		return null;
	}

	@Override
	public long getItemId(int arg0) {
		// TODO 自動生成的方法存根
		return 0;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		// 根據Position分別獲取容器當中存放的每條微博的詳情
		final BlogInfo info = blogsDownLoad.get(position);
		if (convertView == null) {
			// init item view
			convertView = 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("")) {
				// 添加該條微博對應圖片數量的的ImageView
				String[] 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() {
			@Override
			public void onClick(View v) {
				// do something there...
			}
		});
		// 舉報按鈕
		holder.btn_report.setOnClickListener(new View.OnClickListener() {
			@Override
			public 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;
		// 刪除原來的ImageView
		if (imageMap != null && imageMap.size() > 0) {
			holder.images_layout.removeAllViews();
			imageMap = new HashMap<String, ImageView>();
		}
	}
}



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




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章