记,基于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,如果有初学者有兴趣,可以留言告诉我,我会发送您的邮箱里~
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。