- 前言
Android应用出现crash时,会出现“程序异常退出”的提示,随后应用关闭,用户体验非常不好。一般为了捕获应用运行时异常后做出适当处理,给出合理提示时,我们开发中可以继承UncaughtExceptionHandler类来处理,提升用户体验。
2.使用方法
(1)定义CrashHandler处理类,如果发生异常,则在本地文件中记录堆栈信息,代码如下所示:
package com.broadengate.cloudcentral.util;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import com.broadengate.cloudcentral.constant.Global;
/**
*
* <UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告>
* <功能详细描述>
*
* @author cyf
* @version [版本号, 2014-6-17]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class CrashHandler implements UncaughtExceptionHandler
{
public static final String TAG = "CrashHandler";
//CrashHandler 实例
private static CrashHandler INSTANCE = new CrashHandler();
//context
private Context mContext;
private Thread.UncaughtExceptionHandler mDefaultHandler;
private Map<String, String> infos = new HashMap<String, String>();
private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
private CrashHandler()
{
};
public static CrashHandler getInstance()
{
return INSTANCE;
}
public void init(Context context)
{
mContext = context;
// 获取系统默认的 UncaughtException 处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
// 设置该 CrashHandler 为程序的默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当 UncaughtException 发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex)
{
if (!handleException(ex) && mDefaultHandler != null)
{
// 如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
}
else
{
try
{
thread.sleep(4000);
}
catch (InterruptedException e)
{
Log.e(TAG, "error:", e);
}
// 退出程序,注释下面的重启启动程序代码
Global.logoutApplication(mContext);
}
}
/**
*
* <自定义错误处理,收集错误信息,发送错误报告等操作均在此完成>
* <功能详细描述>
* @param ex
* @return
* @see [类、类#方法、类#成员]
*/
private boolean handleException(Throwable ex)
{
if (ex == null)
{
return false;
}
//使用Toast 来显示异常信息
new Thread()
{
@Override
public void run()
{
Looper.prepare();
Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
collectDeviceInfo(mContext);
saveCrashInfo2File(ex);
return true;
}
/**
*
* <手机设备参数信息-收集设备信息>
* <功能详细描述>
* @param ctx
* @see [类、类#方法、类#成员]
*/
public void collectDeviceInfo(Context ctx)
{
try
{
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null)
{
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = pi.versionCode + "";
infos.put("versionName", versionName);
infos.put("versionCode", versionCode);
}
}
catch (NameNotFoundException e)
{
Log.e(TAG, "an error occured when collect package info", e);
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields)
{
try
{
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
Log.d(TAG, field.getName() + ":" + field.get(null));
}
catch (Exception e)
{
Log.e(TAG, "an error occured when collect crash info", e);
}
}
}
/**
*
* <保存错误信息到文件中>
* <功能详细描述>
* @param ex
* @return
* @see [类、类#方法、类#成员]
*/
private void saveCrashInfo2File(Throwable ex)
{
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet())
{
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null)
{
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
sb.append(result);
try
{
String time = formatter.format(new Date());
String fileName = FileSystemManager.getCrashPath(mContext) + time + ".log";
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(sb.toString().getBytes());
fos.close();
}
catch (Exception e)
{
Log.e(TAG, "an error occured while writing file...", e);
}
}
}
可以看出String fileName = FileSystemManager.getCrashPath(mContext) + time + “.log”;文件名用时间戳作为关键字段,并保存在本地,等待上传。
收集错误信息,发送错误报告等操作均在handleException中完成,通过上面的代码可以看到,一旦应用发生崩溃,就会自动调用uncaughtException方法,然后调用handleException方法完成收集手机信息,保存崩溃堆栈至本地,并且清理当前应用activity堆栈,最后自动关闭应用,并给出toast提示。
(2)Application中配置是否打开全局补获异常
package com.broadengate.cloudcentral;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import cn.jpush.android.api.JPushInterface;
import com.broadengate.cloudcentral.constant.Global;
import com.broadengate.cloudcentral.sharepref.SharePref;
import com.broadengate.cloudcentral.util.CMLog;
import com.broadengate.cloudcentral.util.CrashHandler;
import com.broadengate.cloudcentral.util.FileSystemManager;
import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache;
import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator;
import com.nostra13.universalimageloader.cache.memory.impl.LruMemoryCache;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.QueueProcessingType;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.core.display.RoundedBitmapDisplayer;
/**
*
* <应用初始化> <功能详细描述>
*
* @author cyf
* @version [版本号, 2014-3-24]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class CCApplication extends Application
{
/**
* app实例
*/
public static CCApplication ccApplication = null;
/**
* 本地activity栈
*/
public static List<Activity> activitys = new ArrayList<Activity>();
/**
* 加解密密钥
*/
public static String key = "";
@Override
public void onCreate()
{
super.onCreate();
ccApplication = this;
new SharePref(ccApplication).saveAppOpenOrClose(true);
loadData(getApplicationContext());
JPushInterface.setDebugMode(false); // 设置开启日志,发布时请关闭日志
JPushInterface.init(this); // 初始化 JPush
CrashHandler crashHandler = CrashHandler.getInstance();//打开全局异常捕获
crashHandler.init(getApplicationContext());
}
public static void loadData(Context context)
{
// This configuration tuning is custom. You can tune every option, you may tune some of them,
// or you can create default configuration by
// ImageLoaderConfiguration.createDefault(this);
// method.
ImageLoaderConfiguration config =
new ImageLoaderConfiguration.Builder(context).threadPriority(Thread.NORM_PRIORITY - 2)
.threadPoolSize(4)
.tasksProcessingOrder(QueueProcessingType.FIFO)
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(4 * 1024 * 1024))
.discCacheSize(50 * 1024 * 1024)
.denyCacheImageMultipleSizesInMemory()
.discCacheFileNameGenerator(new Md5FileNameGenerator())
.tasksProcessingOrder(QueueProcessingType.LIFO)
.discCache(new UnlimitedDiscCache(new File(FileSystemManager.getCacheImgFilePath(context))))
.build();
ImageLoader.getInstance().init(config);
}
@Override
public void onTerminate()
{
super.onTerminate();
try
{
CCApplication.key = "";
Global.setUserId("");
Global.setStore("");
Global.setLogin(false);
//保存到配置文件
SharePref preference = new SharePref(ccApplication);
preference.saveInitFlag(false);
preference.saveIsLogin(false);
preference.saveAppOpenOrClose(false);
for (Activity activity : activitys)
{
activity.finish();
}
}
catch (Exception e)
{
CMLog.e("", "finish activity exception:" + e.getMessage());
}
finally
{
ImageLoader.getInstance().clearMemoryCache();
System.exit(0);
}
}
/**
*
* <添加> <功能详细描述>
*
* @param activity
* @see [类、类#方法、类#成员]
*/
public void addActivity(Activity activity)
{
activitys.add(activity);
}
}
(3)下一次启动后,上传上次保存在本地的奔溃文件,上传成功后,删除,避免重复上传。
/**
*
* <上传崩溃日志文件到服务器>
* <功能详细描述>
* @see [类、类#方法、类#成员]
*/
private void uploadCrashFile()
{
File root = new File(FileSystemManager.getCrashPath(HomeFragmentActivity.this));
File[] listFiles = root.listFiles();
if (listFiles.length > 0)
{
ArrayList<File> files = new ArrayList<File>();
for (File file : listFiles)
{
files.add(file);
}
Map<String, List<File>> fileParameters = new HashMap<String, List<File>>();
fileParameters.put("file", files);
ConnectService.instance().connectServiceUploadCrashFile(HomeFragmentActivity.this,
null,
fileParameters,
HomeFragmentActivity.this,
UploadCrashFileResponse.class,
URLUtil.UPLOAD_CRASH_FILE);
}
}
上传成功后的代码
/**
* 向服务器上传崩溃文件
*/
else if (ob instanceof UploadCrashFileResponse)
{
UploadCrashFileResponse uploadCrashFileResponse = (UploadCrashFileResponse)ob;
if (GeneralUtils.isNotNullOrZeroLenght(uploadCrashFileResponse.getRetcode()))
{
if (Constants.SUCESS_CODE.equals(uploadCrashFileResponse.getRetcode()))
{
//发送成功后删除本地文件
FileUtil.deleteDirectory(FileSystemManager.getCrashPath(HomeFragmentActivity.this));
}
}
}
(4)文件夹公共类
package com.broadengate.cloudcentral.util;
import java.io.File;
import android.content.Context;
/**
*
* <管理本地文件目录>
* <功能详细描述>
*
* @author cyf
* @version [版本号, 2014-6-30]
* @see [相关类/方法]
* @since [产品/模块版本]
*/
public class FileSystemManager
{
/**
* 根目录缓存目录
*/
private static String cacheFilePath;
/**
* 列表页面图片缓存目录
*/
private static String cacheImgFilePath;
/**
* 用户头像缓存目录
*/
private static String userHeadPath;
/**
* 省市区数据库缓存目录
*/
private static String dbPath;
/**
* 投诉维权图片缓存目录
*/
private static String mallComplaintsPicPath;
/**
* 投诉维权语音缓存目录
*/
private static String mallComplaintsVoicePath;
/**
* 崩溃日志缓存目录(上传成功则删除)
*/
private static String crashPath;
/**
* 圈子发帖缓存目录(删除上次发帖缓存,保存本次发帖纪录)
*/
private static String postPath;
/**
* 临时目录
*/
private static String temporaryPath;
/**
*
* <根目录缓存目录>
* <功能详细描述>
* @param context
* @return
* @see [类、类#方法、类#成员]
*/
public static String getCacheFilePath(Context context)
{
cacheFilePath = FileUtil.getSDPath(context) + File.separator + "cloudcentral" + File.separator;
return cacheFilePath;
}
/**
*
* <列表页面图片缓存目录>
* <功能详细描述>
* @param context
* @return
* @see [类、类#方法、类#成员]
*/
public static String getCacheImgFilePath(Context context)
{
String path = getCacheFilePath(context) + "img" + File.separator;
cacheImgFilePath = FileUtil.createNewFile(path);
FileUtil.createNewFile(path + ".nomedia");
return cacheImgFilePath;
}
/**
*
* <用户头像缓存目录>
* <功能详细描述>
* @param context
* @param userId
* @return
* @see [类、类#方法、类#成员]
*/
public static String getUserHeadPath(Context context, String userId)
{
userHeadPath =
FileUtil.createNewFile(getCacheFilePath(context) + "head" + File.separator + userId + File.separator);
return userHeadPath;
}
/**
*
* <省市区数据库缓存目录>
* <功能详细描述>
* @param context
* @return
* @see [类、类#方法、类#成员]
*/
public static String getDbPath(Context context)
{
dbPath = FileUtil.createNewFile(getCacheFilePath(context) + "database" + File.separator);
return dbPath;
}
/**
*
* <投诉维权图片缓存目录>
* <功能详细描述>
* @param context
* @param userId
* @return
* @see [类、类#方法、类#成员]
*/
public static String getMallComplaintsPicPath(Context context, String userId)
{
mallComplaintsPicPath =
FileUtil.createNewFile(getCacheFilePath(context) + "mallcomplaints" + File.separator + userId
+ File.separator + "pic" + File.separator);
return mallComplaintsPicPath;
}
/**
*
* <投诉维权语音缓存目录>
* <功能详细描述>
* @param context
* @param userId
* @return
* @see [类、类#方法、类#成员]
*/
public static String getMallComplaintsVoicePath(Context context, String userId)
{
mallComplaintsVoicePath =
FileUtil.createNewFile(getCacheFilePath(context) + "mallcomplaints" + File.separator + userId
+ File.separator + "voice" + File.separator);
return mallComplaintsVoicePath;
}
/**
*
* <崩溃日志缓存目录(上传成功则删除)>
* <功能详细描述>
* @param context
* @return
* @see [类、类#方法、类#成员]
*/
public static String getCrashPath(Context context)
{
crashPath = FileUtil.createNewFile(getCacheFilePath(context) + "crash" + File.separator);
return crashPath;
}
/**
*
* <圈子发帖缓存目录(删除上次发帖缓存,保存本次发帖纪录)>
* <功能详细描述>
* @param context
* @return
* @see [类、类#方法、类#成员]
*/
public static String getPostPath(Context context)
{
postPath = FileUtil.createNewFile(getCacheFilePath(context) + "post" + File.separator);
return postPath;
}
/**
*
* <临时图片缓存目录>
* <功能详细描述>
* @param context
* @return
* @see [类、类#方法、类#成员]
*/
public static String getTemporaryPath(Context context)
{
temporaryPath = FileUtil.createNewFile(getCacheFilePath(context) + "temp" + File.separator);
return temporaryPath;
}
}
package com.broadengate.cloudcentral.util;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.http.util.EncodingUtils;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.os.Environment;
import android.os.Parcelable;
public class FileUtil
{
// 获取sdcard的目录
public static String getSDPath(Context context)
{
// 判断sdcard是否存在
if (Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
{
// 获取根目录
File sdDir = Environment.getExternalStorageDirectory();
return sdDir.getPath();
}
return "/data/data/" + context.getPackageName();
}
public static String createNewFile(String path)
{
File dir = new File(path);
if (!dir.exists())
{
dir.mkdirs();
}
return path;
}
// 复制文件
public static void copyFile(InputStream inputStream, File targetFile)
throws IOException
{
BufferedOutputStream outBuff = null;
try
{
// 新建文件输出流并对它进行缓冲
outBuff = new BufferedOutputStream(new FileOutputStream(targetFile));
// 缓冲数组
byte[] b = new byte[1024 * 5];
int len;
while ((len = inputStream.read(b)) != -1)
{
outBuff.write(b, 0, len);
}
// 刷新此缓冲的输出流
outBuff.flush();
}
finally
{
// 关闭流
if (inputStream != null)
inputStream.close();
if (outBuff != null)
outBuff.close();
}
}
/**
* 文件是否已存在
*
* @param file
* @return
*/
public static boolean isFileExit(File file)
{
if (file.exists())
{
return true;
}
return false;
}
/**
* 判断指定目录是否有文件存在
*
* @param path
* @param fileName
* @return
*/
public static File getFiles(String path, String fileName)
{
File f = new File(path);
File[] files = f.listFiles();
if (files == null)
{
return null;
}
if (null != fileName && !"".equals(fileName))
{
for (int i = 0; i < files.length; i++)
{
File file = files[i];
if (fileName.equals(file.getName()))
{
return file;
}
}
}
return null;
}
/**
* 根据文件路径获取文件名
*
* @return
*/
public static String getFileName(String path)
{
if (path != null && !"".equals(path.trim()))
{
return path.substring(path.lastIndexOf("/"));
}
return "";
}
// 从asset中读取文件
public static String getFromAssets(Context context, String fileName)
{
String result = "";
try
{
InputStreamReader inputReader = new InputStreamReader(context.getResources().getAssets().open(fileName));
BufferedReader bufReader = new BufferedReader(inputReader);
String line = "";
while ((line = bufReader.readLine()) != null)
result += line;
return result;
}
catch (Exception e)
{
e.printStackTrace();
}
return result;
}
public static String FileInputStreamDemo(String path)
{
try
{
File file = new File(path);
if (!file.exists() || file.isDirectory())
;
FileInputStream fis = new FileInputStream(file);
byte[] buf = new byte[1024];
StringBuffer sb = new StringBuffer();
while ((fis.read(buf)) != -1)
{
sb.append(new String(buf));
buf = new byte[1024];// 重新生成,避免和上次读取的数据重复
}
return sb.toString();
}
catch (Exception e)
{
}
return "";
}
public static String FileInputStreamDemo(String fileName, Context context)
{
try
{
AssetManager aManager = context.getResources().getAssets();
InputStream in = aManager.open(fileName); // 从Assets中的文件获取输入流
int length = in.available(); // 获取文件的字节数
byte[] buffer = new byte[length]; // 创建byte数组
in.read(buffer); // 将文件中的数据读取到byte数组中
String result = EncodingUtils.getString(buffer, "UTF-8");
return result;
}
catch (Exception e)
{
// Log.e("", "error:" + e.getMessage());
}
return "";
}
/**
* 删除目录(文件夹)下的文件
*
* @param sPath
* 被删除目录的文件路径
* @return 目录删除成功返回true,否则返回false
*/
public static void deleteDirectory(String path)
{
File dirFile = new File(path);
File[] files = dirFile.listFiles();
if (files != null && files.length > 0)
{
for (int i = 0; i < files.length; i++)
{
// 删除子文件
if (files[i].isFile())
{
files[i].delete();
}
// 删除子目录
else
{
deleteDirectory(files[i].getAbsolutePath());
}
}
}
}
// 保存序列化的对象到app目录
public static void saveSeriObj(Context context, String fileName, Object o)
throws Exception
{
String path = context.getFilesDir() + "/";
File dir = new File(path);
dir.mkdirs();
File f = new File(dir, fileName);
if (f.exists())
{
f.delete();
}
FileOutputStream os = new FileOutputStream(f);
ObjectOutputStream objectOutputStream = new ObjectOutputStream(os);
objectOutputStream.writeObject(o);
objectOutputStream.close();
os.close();
}
// 读取序列化的对象
public static Object readSeriObject(Context context, String fileName)
throws Exception
{
String path = context.getFilesDir() + "/";
File dir = new File(path);
dir.mkdirs();
File file = new File(dir, fileName);
InputStream is = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(is);
Object o = objectInputStream.readObject();
return o;
}
/**
* 保存camera capture到file
* @return 是否成功
*/
public static boolean savePhoto(Parcelable data, String path)
{
boolean rs = false;
// Bitmap photo = new Intent().getExtras().getParcelable("data");
if (null == data)
{
return rs;
}
try
{
Bitmap photo = (Bitmap)data;
CMLog.w("FileUtil.save", "orign size:"+ photo.getByteCount());
File file = new File(path);
file.createNewFile();
FileOutputStream out = new FileOutputStream(file);
// photo.compress(Bitmap.CompressFormat.PNG, 100, out);
// ByteArrayOutputStream out = new ByteArrayOutputStream();
//TODO 调用BimmapUtil压缩
rs = photo.compress(Bitmap.CompressFormat.JPEG, 100, out);//PNG
out.flush();
out.close();
rs &= true;
CMLog.w("FileUtil.save", "file size:"+ file.length());
}
catch (FileNotFoundException e)
{
e.printStackTrace();
rs = false;
}
catch (IOException e)
{
e.printStackTrace();
rs = false;
}
return rs;
}
/**
* 下载音频文件,先从本地获得,本地没有 再从网络获得
* <一句话功能简述>
* <功能详细描述>
* @param url
* @param context
* @param userId
* @return
* @see [类、类#方法、类#成员]
*/
public static String getAudiaFile(String url, Context context, String userId)
{
String audioName = url.substring(url.lastIndexOf("/") + 1, url.length());
File file = new File(FileSystemManager.getMallComplaintsVoicePath(context, userId), audioName);
if (file.exists())
{
return file.getPath();
}
return loadImageFromUrl(context,url,file);
}
public static String loadImageFromUrl(Context context,String imageURL, File file)
{
try
{
URL url = new URL(imageURL);
HttpURLConnection con = (HttpURLConnection)url.openConnection();
con.setConnectTimeout(5*1000);
con.setDoInput(true);
con.connect();
if (con.getResponseCode() == 200)
{
InputStream inputStream = con.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024*20];
int length = -1;
while((length = inputStream.read(buffer)) != -1)
{
byteArrayOutputStream.write(buffer,0,length);
}
byteArrayOutputStream.close();
inputStream.close();
FileOutputStream outputStream = new FileOutputStream(file);
outputStream.write(byteArrayOutputStream.toByteArray());
outputStream.close();
return file.getPath();
}
}
catch (MalformedURLException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
return "";
}
}
如有错误欢迎指出来,一起学习。