记,基于Android开发类似于微博的东东时,值得记录的几个问题~

作为一个Java的使用者,在经历了Web到服务端开发的工作后,今年终于开始接触一些android开发方面的工作了。

新的挑战~~最近有一个需求是在应用里开发一个类似于微博的功能模块,说难不难,说易不易~~

作为一名Android上的菜鸟,在开发的过程里还是遇到不少问题的。当然,紧接着的就是一个个的想办法解决问题~~~~~

一直想把过程中遇到的,自己觉得几个比较有意义的问题,及其解决方法记录下来,但苦逼的是最近一直没有多的时间~~~

今天又到了一周一度的美好周末,阳光明媚,那干脆起个早,来写一写,一来也给自己加深下印象~~~

另外,如果您也是一个刚刚开始接触Android的菜鸟,希望能给您带去一点帮助。

而同时,如果您看到其中的某处应用不当,或者有更好的实现方式,更希望您能不吝指出,帮助我进步~


问题剖析:


开发类似于微博的这种功能,首先想到的,自然就是会用到ListView。那么,这其中会遇到的几个问题在什么地方呢?


1、首先,与普通的ListView定义不同,像微博这种东西,内容存在“不确定性”。这个不确定性是指什么呢?比如,有的微博内容里可能会带有图片,而有的则可能为纯文本;而在带有图片的微博中,图片的数目也是不确定的。所以说,对于界面的定义,自然就不能再仅仅依靠布局文件了。而需要借助代码在类文件中实现“动态加载控件”。


2、第二个问题,也是很常见的问题,就是在该种界面中,通常会包含大量的图片,例如用户头像,微博内容里的图片等等。这个时候自然就需要新开线程去处理从服务器下载图片,并更新界面的操作。也就是所谓的“图片的异步加载”工作。


3、与之伴随而来的,就是关于图片加载的另一个问题,界面里的图片很多。如果每次加载时,我们都要从服务器去下载,首先的问题就是加载的速度;其次这样的实现方式,对于网络资源的使用,只能说“抵制铺张浪费,从我做起”。那么,对应的,就需要实现“图片的缓存”。


4、最后一个想要记录的问题,是比较有意思的问题,也是过程中让我最蛋疼的问题。那就是Android对于ListView控件的“Recycler”机制,导致图片会出现显示错乱的问题。


针对于这些问题,从床上爬起来理一理思路,重写了一个Demo,大体效果如下:

技术分享    技术分享   技术分享

接着,我们就按照开发这个玩意儿的步骤走一遍,然后看针对于上面提出的几点值得注意的问题,其解决之道是什么?


一、布局文件的定义

正如同建筑师们建造一幢精美的建筑,得先画出设计图纸一样。我们既然要开发一个我们自己的“微博”,那我们就先搞出“微博”界面的布局文件。


但针对于这一点并没有太多值得额外提到的地方,只需要按照自己想要的样式来定义自己的布局文件就行了。



二、类的定义

当我们已经有了“设计图”,接下来就是实际的“建筑工作”了。


首先,我们会定义一个继承于Activity的类来关联我们定义的布局文件。

接着,因为我们所定义的微博内容的界面中,使用了ListView控件。而ListView控件的具体内容,则需要由一个Adapter来提供。所以我们还需要定义一个Adpater类。

这时候,我们上面谈到的第一个问题就来了:“内容的不确定性”。基于存在有的微博可能为纯文本,有的带有图片;带有图片的微博中,有的仅仅只有一张图片,有的可能两张,也有可能更多的这种情况。

那么,针对于图片的显示,我们就应该在代码中进行动态的添加对应数目的“ImageView”。


所以,在我们定义的Adpater中的getView方法中,可能会存在类似于这样的代码:

<span style="font-size:14px;"><span style="font-family:SimSun;">		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);
		} </span></span>

现在,简单的来说,我们已经初步解决了关于“动态加载控件的”问题。

而当我们已经定义好了显示微博内容的Adpater之后。我们马上将要面临的就是上面谈到的下一个问题:“图片的异步加载”。

那么,首先我们需要明确的就是,为什么我们要对图片做异步加载?这是因为:

在Android当中,当一个应用程序的组件启动的时候,并且没有其他的应用程序组件在运行时,Android系统就会为该应用程序组件开辟一个新的线程来执行。

默认的情况下,在一个相同Android应用程序当中,其里面的组件都是运行在同一个线程里面的,这个线程我们称之为Main线程。

当我们通过某个组件来启动另一个组件的时候,这个时候默认都是在同一个线程当中完成的。当然,我们可以自己来管理我们的Android应用的线程,我们可以根据我们自己的需要来给应用程序创建额外的线程。

也就是说,在Android中,对于“应用界面”的管理,都是在主线程当中完成的。所以,永远不要在主线程中做耗时的操作!

在我们这里所说的“微博”来讲,从服务器去下载图片到我们的客户端应用进行显示,这就是一个所谓的耗时操作。更何况,我们下载的图片的数量可能还很大。

那么,如果我们不对其进行“异步下载”的处理,会带来的影响就例如:

直到我们界面上所需要显示的所有图片下载完成之前,主线程一直都处于一个“阻塞”的状态。

而这反应在用户体验上,也就是应用一直处于顿卡状态,无法响应用户其它任何的新的操作。

更糟糕的是,当我们的整个现场如果阻塞时间超过5秒钟(官方是这样说的),这个时候就会出现 ANR (Application Not Responding)的现象,此时,应用程序会弹出一个框,让用户选择是否退出该程序。这当然是糟糕透了的情况。


所以,我们自然会选择对“下载图片”的操作进行“异步实现”。这听上去很高大上的术语,其实原理很简单。

既然不要在主线程当中做耗时的操作,那我们要做的既然就是新开一个辅助线程,到服务器下载图片,当图片下载完成后,再通知主线程更新界面的显示。

Android提供了两种方式来解决线程直接的通信问题,一种是Handler机制,另一种就是AsyncTask机制。


我们这里选择使用AsyncTask机制,来实现所谓的“图片的异步加载”:

<span style="font-size:14px;"><span style="font-family:SimSun;">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);
			}
		}
	}
}
</span></span>
这个类的思路很简单,在该类的构造函数中,我们获取两个参数:

一个是要进行异步加载的图片的URL,我们通过这个URL进行网络下载。

另一个则是在应用中,要将这张加载的图片显示到程序界面上的ImageView控件。

接着,我们在doInBackground方法中,下载这张图片。当图片下载完成后,onPostExecute收到通知,将下载到的图片加载到对应的控件上去。

也就完成了,我们所谓的“图片的异步加载”的工作。


此时,我们已经对图片添加了“异步加载”的处理方式。这很不错,但这显然还远远不够,因为我们还需要解决我们上面谈到的第三个问题:“浪费可耻”!

之所以这样讲,是因为,此时我们对于获取图片的方式仍然只有一种,就是“从网络下载获取”。这样做的结果就是,我们上次下载好的图片,丝毫不具备重用性。

例如:我们此次浏览了一些内容后,退出了应用;又或者我们在不断上下滑动,或刷新着屏幕,基于Android中ListView自身的特点,都需要一次次的去重复下载图片。

这时,我们要做的,就是添加“缓存机制”,当我们从网络中下载好图片之后,就将下载好的图片存放到缓存当中去,当下次需要使用到某张图片资源的时候,我们先到缓存中去查看是否存在,如果存在则直接获取,如果不存在,才到网络上去下载。

这样做的好处很明显,一直为用户节省了“网络资源”,另外也很大程度上的提高了获取资源的速度。这是显而易见的,你家里有一个储物室,当你需要一件物品,先看看家里的储物室里有没有,有则直接拿来使用,没有的话,再驱车去外面的商场购买;和每次一有需求,则开着车跑到老远的地方购买,这其中节约的时间,不言而喻。


废话不说,Android中对于图片的内存缓存,最常使用到的是LruCache。所以,我们进一步改进程序,将“缓存”与“异步”结合起来,所以我们的图片加载工具类,可能变成了下面这样:

<span style="font-size:14px;"><span style="font-family:SimSun;">@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);
			}
		}
	}
}
</span></span>
这个类的实现,正如我们上面所讲的一样,我们首先在内存中开辟一片区域作为图片资源的缓存,每次加载一张图片时,我们都先看看缓存中是否已经有这张图片了,如果没有,我们才会去通过网络进行下载。

当然,这里为了偷懒和仅仅出于一个说明作用,仅仅只是简单的使用了内存缓存。实际开发中,更为科学的来讲,你还可以选择使用“多级缓存”,例如你还可以在本地文件中开辟缓存,实现:首先到内存缓存中查找,如果没有,则到本地文件中查找,如果还没有,再到网络上去下载。这样,就更为合理了。


当然,要十分优秀的实现这样的需求,需要花费不少的精力。所以也可以选择使用一些图片加载框架,例如:Android-Universal-Image-Loader。这些优秀的框架已经帮你实现了各种关于图片处理的需求,你只需要导入一个第三方包,然后调用API就搞定了。



走到此时,对于这样一个类似微博的功能,我们已经实现的算是不错了。但最让人蛋疼问题,也就是上述的第4个也是最后一个问题,就出现了。

你可能会发现这样的情况,本来位于ListView第7行的用户的头像,莫名其妙显示为第1行的用户的头像。然后在你上下滑动屏幕,ListView进行刷新的过程中,你蛋疼的发现:“擦,全尼玛乱套了”。。

而针对于这样的问题,只要你耐心,上网多查查资料,就会初步得到一个解决方案,为显示头像的ImageView控件,添加一个Tag,这个tag的值通常就使用的是这个ImageView对应要显示的图片的URL。

我最开始,也是这样解决的。但问题虽然解决了,我其实还是不没有很明白造成这样的情况的原因。于是当这个问题解决之后,我发现了一个更操作的问题。

上面我们说过了,“微博”的内容存在“不确定性”。于是,我又发现了这样的情况,当我点击加载更多按钮,获取到新的微博信息,然后下拉屏幕的过程中,也许第七条微博是没有图片内容的,但它却莫名其妙的加载出了一个图片内容,而同时你会发现,这个图片内容实际上是前面第二条微博的。

好吧,我只能说,我凌乱了。。。于是继续查资料,功夫不负有心人,终于在一片博客里发现了这个现象发生的原因,也就是所谓的“recycler”机制。

具体说明,可以参照这篇博客:【Android】ListView中getView的原理与解决多轮重复调用的方法


其实看了这明白了这篇博客之后,就会知道:之所以出现这样的错误情况,是因为我们在getView方法中,选择使用了一个viewHolder来帮助我们对界面中的控件进行复用。在这种情况下,我们的getView方法的实现通常类似于这样:

<span style="font-size:14px;"><span style="font-family:SimSun;">	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;
		
	}</span></span>

但是,如果我们不使用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类变为了下面的样子:

<pre name="code" class="java">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>();
		}
	}
}



到了这里,提到的几个问题也讲完了~~~~~

如果有宝贵意见,望多多指教。这篇文章里用到的项目demo,如果有初学者有兴趣,可以留言告诉我,我会发送您的邮箱里~



郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。