您的位置:首页 > 移动开发 > Android开发

Android开发社交软件类应用时的若干问题

2015-03-23 16:16 429 查看
作为一个Java的使用者,在经历了Web到服务端开发的工作后,今年终于开始接触一些android开发方面的工作了。

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

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

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

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

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

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

问题剖析:

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

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

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

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

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

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







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

一、布局文件的定义

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

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

二、类的定义

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

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

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

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

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

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

[java] view
plaincopy

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机制,来实现所谓的“图片的异步加载”:

[java] view
plaincopy

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。所以,我们进一步改进程序,将“缓存”与“异步”结合起来,所以我们的图片加载工具类,可能变成了下面这样:

[java] view
plaincopy

@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方法的实现通常类似于这样:

[java] view
plaincopy

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类变为了下面的样子:

[java] view
plaincopy

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>();

}

}

}



到了这里,提到的几个问题也讲完了~~~~~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐