概述
Android设备可以提供单一的应用程序一定的内存(根据手机不同内存大小也不同),虽然不同的手机提供的内存也不同但都是有限的,而Bitmap很多时候会占用大量内存,尤其是丰富的图片比如照片,因此如果不小心,Bitmap可以快速消耗的内存预算产生下面可怕的异常导致应用程序崩溃:
java.lang.OutofMemoryError: bitmap size exceeds VM budget.
学习如何使用常见的技术来处理和加载位图对象的方式,保持用户界面组件响应,并避免超过应用程序内存限制。
高效加载大图片
编写Android程序经常需要用到许多图片。在大多数情况下,这些图片都会大于我们程序需要的大小。因此在使用图片时,最好对图片进行压缩,压缩后的图片大小应该和用来展示它的控件大小相近。
下面是android官方文档提供的压缩方法:
读取bitmap尺寸和类型
BitmapFactory()类提供了几种解码方法(decodeByteArray(), decodeFile(), decodeResource()等)用于从各种sources中创建bitmap对象。这些方法为构建的bitmap分配内存,因此很容易地导致OutOfMemory异常。为此每一种解析方法都提供了一个可选的BitmapFactory.Options类,通过设置该类的inJustDecodeBounds属性为true来避免在解码时为bitmap分配内存,这时解码方法返回的也不在为一个bitmap对象,而是null(这时显而易见的,既然没有分配内存当然返回null)。虽然返回的为null,还是为BitmapFactory.Options类中的outWidth,outHeight和outMimeType变量赋值,这种技术允许在位图的构建(和内存分配)之前读取图像数据的尺寸和类型。
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap=BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, options);
if (bitmap==null)
{
Log.i(TAG, "onCreate: ");
}
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
查看日志发现输出了 onCreate:
可以发现将inJustDecodeBounds设置为true之后,系统不会为bitmap分配内存。
为了避免java.lang.OutOfMemory exceptions,在解压之前检查bitmap的尺寸,除非你绝对信任来源,保证不会超过可用的内存。
将按比例缩小的位图加载到内存中
现在图像的尺寸已知,我们就可以决定是将完整的图像还是压缩后的图像加载到内存中。
这里需要考虑一些因素:
加载完整图像所需的内存。
考虑你的应用程序的任何其他内存需求,你愿意提供多少内存加载这个图像。
图像被加载到的目标图片或UI组件的尺寸
当前设备的屏幕大小和密度。
例如:一个imageView只为显示128*96像素的缩略图,就不值得将1024*768像素图像加载到图像中。
如果需要压缩图像,就需要在BitmapFactory.Options中对inSampleSize进行设置,例如,一张图片分辨率2048x1536像素,在inSampleSize为4的情况下解码会生成大约512x384像素的位图。将位图下载到内存中大约使用0.75MB,而不是原图的12MB(假定该位图的配置是ARGB_8888),这里有一个方法来计算一个样本大小的值,是一个基于目标宽度和高度的取舍:
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// 原图的宽高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
//原图宽高大于目标宽高时,计算缩小比
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//保证缩小后的图片宽高大于目标图片宽高
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
上述代码是官方文档给出的示例代码,采用2的幂次方缩小的算法。计算inSampleSize的值可以根据需要来设计代码。
使用这个方法,首先需要将BitmapFactory.Options的inJustDecodeBounds属性设置为true,解析一次图片。然后通过上述方法,就可以得到合适的inSampleSize值了。将获取到的inSampleSize值赋值给inSampleSize,并把inJustDecodeBounds设置为false,再次解析,就可以得到压缩后的图片了。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,int reqWidth, int reqHeight) {
// 第一次解码,将inJustDecodeBounds设置为true,获取尺寸信息
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//计算inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 根据设置的inSampleSize信息再次解码获取bitmap,这时inJustDecodeBounds需设置为false
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
通过上述代码,最终将返回压缩之后的图片。
异步处理图像
当数据源是从磁盘或网络位置读取(或内存以外的任何资源),BitmapFactory.decode*方法不应该在UI线程中执行(其实无论来自哪里,最好都不要在UI线程中执行)。因为数据需要加载的时间是不可预测的,取决于各种因素(从磁盘或网络读取的速度,图像的大小,中央处理器的功率等)。如果这些任务中的一个阻塞了用户界面线程,系统标志您的应用程序作为非响应性,造成NOR错误。
不能在UI线程中执行,就需要新开一个线程,在android中常见的是使用AsyncTask方法。
class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{
private WeakReference<ImageView> imageViewReference;//弱引用
private int data=0;
public BitmapWorkerTask(ImageView imageView) {
this.imageViewReference = new WeakReference<ImageView>(imageView);
}
@Override
protected Bitmap doInBackground(Integer... params) {
data=params[0];
return decodeSampledBitmapFromResource(getResources(),data,100,100);
}
@Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference!=null&&bitmap!=null)
{
ImageView imageView=imageViewReference.get();
if (imageView!=null)
{
imageView.setImageBitmap(bitmap);
}
}
}
}
上述代码主要是先在后代处理图片,之后在UI线程中显示,使用弱引用的原因是保证AsyncTask不会阻止GC(Garbage Collector)回收ImageView和任何对imageView的引用。当任务完成时,不会保证ImageView对象仍然存在,因此我们需要在onPostExecute方法中进行检查在。
通过上述方法我们已经实现了有效的加载大图片,然而我们经常会在Listview和GridView中使用图片,而在Listview和GridView中使用上述方式会产生一些问题,产生问题的原因是Listview和GridView的回收机制。为了可以有效的使用内存,在用户进行滚动时,Listview和GridView会重复利用子views。如果每一个子view中使用asyncTask,当它完成时,不能保证相应的子view没有被重复使用,此外,也不能保证异步任务开始的顺序是它们完成的顺序。这样说可能会比较抽象下面将用一个例子来说明。
下面将根据官方文档中的示例代码编写一段异步加载网络图片的代码。如果对异步加载不是很熟悉的话可以观看慕课网异步加载图片视频。
public class MainActivity extends Activity {
private ListView mListView;
//通过该地址获取各个图片的url
private static String url="http://www.imooc.com/api/teacher?type=4&num=30";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView= (ListView) findViewById(R.id.list);
new MyAsyncTask().execute(url);
}
//访问网络,开启一个线程
class MyAsyncTask extends AsyncTask<String,Void,List<String>>
{
@Override
protected List<String> doInBackground(String... params) {
String strJson;
//获取Json格式数据
strJson = readJson(params[0]);
List<String> list=new ArrayList<>();//存放图片地址信息
//解析Json数据
try {
JSONObject jsonObject=new JSONObject(strJson);
JSONArray array=jsonObject.getJSONArray("data");
for (int i = 0; i < array.length(); i++) {
jsonObject=array.getJSONObject(i);
list.add(jsonObject.getString("picSmall"));
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}
@Override
protected void onPostExecute(List<String> lists) {
super.onPostExecute(lists);
MyAdapter adapter=new MyAdapter(MainActivity.this,lists);
mListView.setAdapter(adapter);
}
}
/**
* 获取对应url的json格式数据
* @param url
* @return json格式数据
*/
private String readJson(String url) {
InputStreamReader isr=null;
StringBuffer result = new StringBuffer();
try {
URL u=new URL(url);
HttpURLConnection urlConnection= (HttpURLConnection) u.openConnection();
isr=new InputStreamReader(urlConnection.getInputStream(),"utf-8");
BufferedReader br=new BufferedReader(isr);
String temp="";
while ((temp=br.readLine())!=null)
{
result.append(temp);
}
urlConnection.disconnect();
br.close();
return result.toString();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
finally {
try {
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
上述代码很简单,主要是为了获取图片的地址。
public class MyAdapter extends BaseAdapter{
private List<String> list;
private LayoutInflater inflater;
private ImageFactory imageFactory;
public MyAdapter(Context context,List<String> list) {
this.list = list;
inflater=LayoutInflater.from(context);
imageFactory=new ImageFactory();
}
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder mHolder;
if (convertView==null)
{
mHolder=new Holder();
convertView=inflater.inflate(R.layout.list_item,null);
mHolder.imageView= (ImageView) convertView.findViewById(R.id.image);
convertView.setTag(mHolder);
}else {
mHolder= (Holder) convertView.getTag();
}
imageFactory.loadBitmap(list.get(position),mHolder.imageView);
}
class Holder{
ImageView imageView;
}
}
listView的每个item只显示一张图片
public class ImageFactory {
private Bitmap bitmap;
/**
* 加载图片
* @param url
* @param imageView
*/
public void loadBitmap(String url,ImageView imageView)
{
BitmapWorkTask task=new BitmapWorkTask(imageView);
task.execute(url);
}
/**
*根据传入的url获取图片
* @param url
* @return
*/
public Bitmap getBitmapByUrl(String urlStr)
{
InputStream is=null;
Bitmap bitmap=null;
try {
URL url=new URL(urlStr);
HttpURLConnection connection= (HttpURLConnection) url.openConnection();
is=connection.getInputStream();
bitmap= BitmapFactory.decodeStream(is);
connection.disconnect();
return bitmap;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
public class BitmapWorkTask extends AsyncTask<String,Void,Bitmap> {
public String url;
private WeakReference<ImageView> imageViewWeakReference;
public BitmapWorkTask(ImageView imageView) {
imageViewWeakReference=new WeakReference<ImageView>(imageView);
}
@Override
protected Bitmap doInBackground(String... params) {
url=params[0];//获取传入的url
Bitmap bitmap=getBitmapByUrl(url);//根据url获取bitmap
return bitmap;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (isCancelled())
{
bitmap=null;
}
if (imageViewWeakReference!=null&&bitmap!=null)
{
ImageView imageView=imageViewWeakReference.get();
if (imageView!=null)
{
imageView.setImageBitmap(bitmap);
}
}
}
}
}
上述例子很简单,通过url获取网络图片,并将这些图片通过listview显示,运行上述代码,会发现一开始加载图片并没有问题,当我们不断滑动listView时会发现每个Item在显示自己的图片时,会先显示另外一张,再显示应该显现的图片,甚至还会出现item显示的图片不是应该显示的图片。这就是上面提到的问题。
如何解决这个问题呢,官方文档给出了一个解决方法。(视频中的setTag方法也很好)
class AsyncDrawable extends BitmapDrawable
{
private WeakReference<BitmapWorkTask> bitmapWorkTaskWeakReference;
public AsyncDrawable(Bitmap bitmap,BitmapWorkTask task)
{
super(bitmap);
bitmapWorkTaskWeakReference=new WeakReference<BitmapWorkTask>(task);
}
public BitmapWorkTask getBitmapWorkerTask()
{
return bitmapWorkTaskWeakReference.get();
}
}
将imageview与BitmapWorkTask 绑定,用于同步。
在执行BitmapWorkerTask之前,先创建一个AsyncDrawable并将它与目标ImageView绑定
ublic void loadBitmap(String url,ImageView imageView)
{
if (cancelPotentialWork(url,imageView))
{
BitmapWorkTask task=new BitmapWorkTask(imageView);
//将BitmapWorkerTask与目标ImageView绑定
AsyncDrawable asyncDrawable=new AsyncDrawable(bitmap,task);
imageView.setImageDrawable(asyncDrawable);
task.execute(url);
}
}
cancelPotentialWork方法对相应的ImageView进行检查,判断ImageView是否有关联其他正在运行的任务。
public boolean cancelPotentialWork(String url,ImageView imageView)
{
//获取imageView关联的BitmapWorkTask
BitmapWorkTask bitmapWorkTask=getBitmapWorkTask(imageView);
if (bitmapWorkTask!=null)
{
String urlStr=bitmapWorkTask.url;
//如果url发生变化,说明imageView被重用,取消任务
if (url!=urlStr)
{
bitmapWorkTask.cancel(true);
}else {
return false;
}
}
//没有任务与imageView关联,或存在的任务已取消
return true;
}
public BitmapWorkTask getBitmapWorkTask(ImageView imageView)
{
if (imageView!=null)
{
Drawable drawable=imageView.getDrawable();
if (drawable instanceof AsyncDrawable)
{
BitmapWorkTask task=((AsyncDrawable) drawable).getBitmapWorkerTask();
return task;
}
}
return null;
}
再次运行,发现不在出现前面的问题,然而在listview中加载图片,发现由于滑动和listview本身的回收机制,导致我们需要多次加载图片,如果每次都从网络上加载图片不仅费时还费流量,因此还需要使用缓存。
使用内存缓存
LruCache类(LRU:Least Recently Used,最近最少使用算法)特别适合缓存bitmap的任务,保持最近引用的对象在一个强引用LinkedHashMap中,当超过指定的缓存限制时删除最近最少使用的对象。
lruCache源码解析
可以发现LruCache需要设定一个缓存限制,为了选择合适的大小,我们需要考虑一些因素(在官方文档中给了很多因素)。没有一个特定的大小或公式适合所有的应用程序,它是由你来分析你的使用,并提出一个合适的解决方案。太小的缓存会导致额外的开销,而没有任何好处,一个过大的缓存可再次引起java.lang.outofmemory,并且使你的应用程序只能在剩下的内存中。
public class ImageFactory {
private LruCache<String,Bitmap> mLruCache;
public ImageFactory() {
//获取最大可用的虚拟机内存,超过会抛出OutOfMemory exception
int maxMemory= (int) (Runtime.getRuntime().maxMemory()/1024);
//使用可用内存的1/8作为内存缓存
int cacheSize=maxMemory/8;
mLruCache=new LruCache<String,Bitmap>(cacheSize)
{
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount()/1024;
}
};
}
public void addBitmapToMemoryCache(String url,Bitmap bitmap)
{
if (getBitmapFromMemoryCache(url)==null)
{
mLruCache.put(url,bitmap);
}
}
public Bitmap getBitmapFromMemoryCache(String url)
{
return mLruCache.get(url);
}
。。。。
}
public void loadBitmap(String url,ImageView imageView)
{
Bitmap bitmap=getBitmapFromMemoryCache(url);
if (bitmap!=null)
{
imageView.setImageBitmap(bitmap);
}else {
if (cancelPotentialWork(url,imageView))
{
BitmapWorkTask task=new BitmapWorkTask(imageView);
AsyncDrawable asyncDrawable=new AsyncDrawable(bitmap,task);
imageView.setImageDrawable(asyncDrawable);
task.execute(url);
}
}
}
首先检查LruCache是否缓存了该bitmap,如果缓存了,则imageView直接加载,否则启动一个新任务来获取图片。
public class BitmapWorkTask extends AsyncTask<String,Void,Bitmap> {
。。。。
@Override
protected Bitmap doInBackground(String... params) {
url=params[0];
Bitmap bitmap=getBitmapByUrl(url);
addBitmapToMemoryCache(url,bitmap);
return bitmap;
}
。。。
}
内存缓存可以让组件快速地重新加载和处理图片,加载很多图片的时候可以提高响应速度和流畅性。但LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。因此Google又提供了一套硬盘缓存的解决方案。
磁盘缓存
官方文档中给出的例子中,DiskLruCache调用了put方法,查看了DiskLruCache的源码却没有找到该方法的实现(不知道为什么),因此关于这部分的内容查看了郭神的博客。
阅读了郭神写的文章后,在代码中添加了磁盘缓存
根据官方文档给的地址,下载DiskLruCache的源码
public class ImageFactory {
private Bitmap bitmap;
private LruCache<String,Bitmap> mLruCache;
private DiskLruCache mDiskLruCache;
private static final int DISK_CACHE_SIZE=1024*1024*10;
public ImageFactory(Context context) {
int maxMemory= (int) (Runtime.getRuntime().maxMemory()/1024);
int cacheSize=maxMemory/8;
mLruCache=new LruCache<String,Bitmap>(cacheSize)
{
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount()/1024;
}
};
//获得缓存地址
File cacheDir=getDiskCacheDir(context,"bitmap");
if (!cacheDir.exists())
{
cacheDir.mkdirs();
}
try {
mDiskLruCache=DiskLruCache.open(cacheDir,getAppVersion(context),1,DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 添加图片到内存缓存
* @param url
* @param bitmap
*/
public void addBitmapToMemoryCache(String url,Bitmap bitmap)
{
if (getBitmapFromMemoryCache(url)==null)
{
mLruCache.put(url, bitmap);
}
}
/**
* 从内存缓存中获取图片
* @param url
* @return
*/
public Bitmap getBitmapFromMemoryCache(String url)
{
return mLruCache.get(url);
}
class AsyncDrawable extends BitmapDrawable
{
private WeakReference<BitmapWorkTask> bitmapWorkTaskWeakReference;
public AsyncDrawable(Bitmap bitmap,BitmapWorkTask task)
{
super(bitmap);
bitmapWorkTaskWeakReference=new WeakReference<BitmapWorkTask>(task);
}
public BitmapWorkTask getBitmapWorkerTask()
{
return bitmapWorkTaskWeakReference.get();
}
}
/**
* 加载图片
* @param url
* @param imageView
*/
public void loadBitmap(String url,ImageView imageView)
{
Bitmap bitmap=getBitmapFromMemoryCache(url);
if (bitmap!=null)
{
imageView.setImageBitmap(bitmap);
}else {
if (cancelPotentialWork(url,imageView))
{
BitmapWorkTask task=new BitmapWorkTask(imageView);
AsyncDrawable asyncDrawable=new AsyncDrawable(bitmap,task);
imageView.setImageDrawable(asyncDrawable);
task.execute(url);
}
}
}
/**
*根据传入的url获取图片
* @param url
* @param imageView
* @return
*/
public boolean cancelPotentialWork(String url,ImageView imageView)
{
BitmapWorkTask bitmapWorkTask=getBitmapWorkTask(imageView);
if (bitmapWorkTask!=null)
{
String urlStr=bitmapWorkTask.url;
if (url!=urlStr)
{
bitmapWorkTask.cancel(true);
}else {
return false;
}
}
return true;
}
public BitmapWorkTask getBitmapWorkTask(ImageView imageView)
{
if (imageView!=null)
{
Drawable drawable=imageView.getDrawable();
if (drawable instanceof AsyncDrawable)
{
BitmapWorkTask task=((AsyncDrawable) drawable).getBitmapWorkerTask();
return task;
}
}
return null;
}
public class BitmapWorkTask extends AsyncTask<String,Void,Bitmap> {
public String url;
private WeakReference<ImageView> imageViewWeakReference;
public BitmapWorkTask(ImageView imageView) {
imageViewWeakReference=new WeakReference<ImageView>(imageView);
}
@Override
protected Bitmap doInBackground(String... params) {
url=params[0];
DiskLruCache.Snapshot snapShot=null;
FileDescriptor fileDescriptor = null;
FileInputStream fileInputStream = null;
try {
String key=hashKeyForDisk(url);
snapShot=mDiskLruCache.get(key);
if (snapShot==null)
{
DiskLruCache.Editor editor=mDiskLruCache.edit(key);
if (editor!=null)
{
OutputStream outputStream=editor.newOutputStream(0);
if (downloadUrlToStream(url,outputStream))
{
editor.commit();
}
else {
editor.abort();
}
}
snapShot=mDiskLruCache.get(key);
}
if (snapShot!=null)
{
fileInputStream = (FileInputStream) snapShot.getInputStream(0);
fileDescriptor = fileInputStream.getFD();
}
Bitmap bm = null;
if (fileDescriptor!=null)
{
bm=BitmapFactory.decodeFileDescriptor(fileDescriptor);
}
if (bm != null) {
// 将Bitmap对象添加到内存缓存当中
addBitmapToMemoryCache(url,bm);
}
return bm;
} catch (IOException e) {
e.printStackTrace();
}finally {
if (fileDescriptor == null && fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
}
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
if (isCancelled())
{
bitmap=null;
}
if (imageViewWeakReference!=null&&bitmap!=null)
{
ImageView imageView=imageViewWeakReference.get();
if (imageView!=null)
{
imageView.setImageBitmap(bitmap);
}
}
}
}
/**
* 获取缓存路径
* @param context
* @param uniqueName
* @return
*/
public File getDiskCacheDir(Context context,String uniqueName)
{
String cachePath;
if (Environment.MEDIA_MOUNTED.
equals(Environment.getExternalStorageState())
||!Environment.isExternalStorageRemovable())
{
cachePath=context.getExternalCacheDir().getPath();
}else {
cachePath=context.getCacheDir().getPath();
}
return new File(cachePath+File.separator+uniqueName);
}
/**
* 获得版本号
* @param context
* @return
*/
public int getAppVersion(Context context)
{
try {
PackageInfo info=context.getPackageManager()
.getPackageInfo(context.getPackageName(),0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
/**
* 将传入的key进行MD5编码
* @param key
* @return
*/
public String hashKeyForDisk(String key)
{
String cacheKey;
try {
MessageDigest mDigest=MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey=bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey=String.valueOf(key.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes)
{
StringBuilder sb=new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex=Integer.toHexString(0xFF&bytes[i]);
if (hex.length()==1)
{
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
/**
* 写入磁盘缓存
* @param urlStr
* @param os
* @return
*/
private boolean downloadUrlToStream(String urlStr,OutputStream os)
{
HttpURLConnection conn=null;
BufferedInputStream in=null;
BufferedOutputStream out=null;
try {
URL url = new URL(urlStr);
conn= (HttpURLConnection) url.openConnection();
in=new BufferedInputStream(conn.getInputStream());
out=new BufferedOutputStream(os);
int b;
while((b=in.read())!=-1)
{
out.write(b);
}
return true;
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if (conn!=null)
{
conn.disconnect();
}
try {
if (out!=null)
{
out.close();
}
if (in!=null)
{
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}