Android使用UncaughtExceptionHandler捕获全局异常

1. 前言

Android应用出现crash时,会出现“程序异常退出”的提示,随后应用关闭,用户体验很是很差。通常为了捕获应用运行时异常后作出适当处理,给出合理提示时,咱们开发中能够继承UncaughtExceptionHandler类来处理,提高用户体验。java

2.使用方法

(1)定义CrashHandler处理类,若是发生异常,则在本地文件中记录堆栈信息,代码以下所示:android

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提示。web

(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)下一次启动后,上传上次保存在本地的奔溃文件,上传成功后,删除,避免重复上传。apache

/** * * <上传崩溃日志文件到服务器> * <功能详细描述> * @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);
        }
    }

上传成功后的代码api

/** * 向服务器上传崩溃文件 */
        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 "";
    }
}