Android-AsyncTask及UncaughtExceptionHandler捕獲全域性性異常(ANR、FC)

desaco發表於2016-01-27
 為了不阻塞UI執行緒(亦稱主執行緒),提高應用的響應性,我們經常會使用新開執行緒的方式,非同步處理那些導致阻塞的任務(如要了解Android非同步處理的實現方式和原理,請先閱讀Android非同步處理系列文章索引》)。

 AsyncTask是Android為我們提供的方便編寫非同步任務的工具類,但是,在瞭解AsyncTask的實現原理之後,發現AsyncTask並不能滿足我們所有的需求,使用不當還有可能導致應用FC。

 》AsyncTask類包含一個全域性靜態的執行緒池,執行緒池的配置引數如下:
private static final int CORE_POOL_SIZE =5;//5個核心工作執行緒  
   private static final int MAXIMUM_POOL_SIZE = 128;//最多128個工作執行緒  
   private static final int KEEP_ALIVE = 1;//空閒執行緒的超時時間為1秒  
  
   private static final BlockingQueue<Runnable> sWorkQueue = 
           new LinkedBlockingQueue<Runnable>(10);//等待佇列  
   private static final ThreadPoolExecutorsExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, 
           MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue,sThreadFactory);//執行緒池是靜態變數,所有的非同步任務都會放到這個執行緒池的工作執行緒內執行。 

》 問題:Android的裝置一般不超過2個cpu核心,過多的執行緒會造成執行緒間切換頻繁,消耗系統資源。

。。執行緒池中已經有128個執行緒,緩衝佇列已滿,如果此時向執行緒提交任務,將會丟擲RejectedExecutionException。

問題:丟擲的錯誤不catch的話會導致程式FC。

AsyncTask可能存在新開大量執行緒消耗系統資源和導致應用FC的風險,因此,我們需要根據自己的需求自定義不同的執行緒池。

(1)、AsyncTask是封裝好的執行緒池,比起Thread+Handler的方式,AsyncTask在操作UI執行緒上更方便,因為onPreExecute()、onPostExecute()及更新UI方法onProgressUpdate()均執行在主執行緒中,這樣就不用Handler發訊息處理了; 
(2)、我不太同意封裝好就會影響效能的說法,在我實際的運用中,真正的缺點來自於AsyncTask的全域性執行緒池只有5個工作執行緒,也就是說,一個APP如果運用AsyncTask技術來執行執行緒,那麼同一時間最多隻能有5個執行緒同時執行,其他執行緒將被阻塞(注:不運用AsyncTask執行的執行緒,也就是自己new出來的執行緒不受此限制),所以AsyncTask不要用於多執行緒取網路資料,因為很可能這樣會產生阻塞,從而降低效率。

》能否同時併發100+asynctask呢? 

      AsyncTask用的是執行緒池機制,容量是128,最多同時執行5個core執行緒,剩下的排隊。 

》 導致出現Force Close的原因有很多,常見的有比如空指標啦,類沒有找到啦,資源沒找到,就連Android API使用的順序錯誤也可能導致(比如     setContentView()之前進行了findViewById()操作)

     Force Close有的人說可以用來讓應用完全退出 而故意導致這個問題,讓程式強制關閉,這種做法我還是不常用。

如何避免彈出Force Close視窗 可以實現Thread.UncaughtExceptionHandler介面的uncaughtException方法 程式碼如下

    importjava.lang.Thread.UncaughtExceptionHandler;

    import android.app.Application;

    public class MyApplication  extends Application implements UncaughtExceptionHandler {

    @Override

    public voidonCreate() {

      // TODOAuto-generated method stub

      super.onCreate();

    }

    @Override

    public void  uncaughtException(Thread thread, Throwable ex) {

      thread.setDefaultUncaughtExceptionHandler(this)

    }

    }

想要哪個執行緒可以處理未捕獲異常,thread.setDefaultUncaughtExceptionHandler( this); 這句程式碼都要在那個執行緒中執行一次

我們需要進行全域性性的異常捕獲,那麼如何捕獲全域性異常呢?

答案是 UncaughtExceptionHandler+Thread.setDefaultUncaughtExceptionHandler

1.UncaughtExceptionHandler是未捕獲異常的處理介面,該類率先捕獲異常

 UncaughtExceptionHandler: 執行緒未捕獲異常控制器是用來處理未捕獲異常的。  
                            如果程式出現了未捕獲異常預設情況下則會出現強行關閉對話方塊 
                            實現該介面並註冊為程式中的預設未捕獲異常處理  
                            這樣當未捕獲異常發生時,就可以做些異常處理操作 
                            例如:收集異常資訊,傳送錯誤報告 等。

對於這個介面,我們需要進行實現

public class AppCrashHandler implements UncaughtExceptionHandler {	
	private Context mContext;
    private Thread.UncaughtExceptionHandler mDefaultHandler;
    /**防止多執行緒中的異常導致讀寫不同步問題的lock**/
    private Lock lock = null;
	/**本地儲存檔案日誌**/
	private final String CRASH_REPORTER_EXTENSION = ".crash";
    /**日誌tag**/
    private final String STACK_TRACE = "logStackTrance";
    /**儲存檔名**/
	private final String crash_pref_path ="app_crash_pref.xml";
    private AppCrashHandler()
    {
        lock = new ReentrantLock(true);   
    }
    /**
     * 獲得單例物件
     * @param context
     * @return AppCrashHandler
     */
	public static AppCrashHandler shareInstance(Context context){
		 AppCrashHandler crashhandler = AppCrashHandler.InstanceHolder.crashHandler;
		 crashhandler.initCrashHandler(context);
		 return crashhandler;
	}
	/**
	 * 使用初始化方法初始化,防止提前初始化或者重複初始化
	 * @param cxt
	 */
	private void initCrashHandler(Context cxt)
	{
		if(!hasInitilized()){
			mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
			Thread.setDefaultUncaughtExceptionHandler(this);
			mContext = cxt; 
		}	
	}
	public interface InstanceHolder
	{
		public static AppCrashHandler crashHandler = new AppCrashHandler();
	}
	@Override
	public void uncaughtException(Thread thread, Throwable ex) {
		 if (!handleExceptionMessage(ex) && mDefaultHandler != null) {  
	            // 如果使用者沒有處理則讓系統預設的異常處理器來處理  
	            mDefaultHandler.uncaughtException(thread, ex);  
	        } else {  
	            try {  
	                Thread.sleep(3000);  
	            } catch (InterruptedException e) {  
	                Log.e(STACK_TRACE, "Error : ", e);  
	            }  
	            android.os.Process.killProcess(android.os.Process.myPid());  
	            System.exit(10);  
	     }  
	}
	/** 
     * 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成. 開發者可以根據自己的情況來自定義異常處理邏輯 
     * @param ex 
     * @return true:如果處理了該異常資訊;否則返回false 
     */  
    private boolean handleExceptionMessage(Throwable ex) 
    {  
        if (ex == null) 
        {  
            return false;  
        }  
        // 使用Toast來顯示異常資訊  
        new Thread() {  
            @Override  
            public void run() {  
                // Toast 顯示需要出現在一個執行緒的訊息佇列中  
                Looper.prepare();  
                Toast.makeText(mContext, "程式出錯啦,即將退出", Toast.LENGTH_LONG).show();  
                Looper.loop();  
            }  
        }.start();  
        String fileName = mContext.getPackageName()+"-"+"appCrash-Exception"+ CRASH_REPORTER_EXTENSION;  
        String crashFileName = saveExceptionToFile(ex,fileName); 
		SharedPreferences.Editor editor = mContext.getSharedPreferences(crash_pref_path , Context.MODE_PRIVATE).edit();
		editor.putString(STACK_TRACE, crashFileName);
		editor.commit();
		Log.d(STACK_TRACE, "errorLogPath="+crashFileName);
        return true;  
    }  
    /**
     * 是否已初始化
     * @return
     */
    public boolean hasInitilized()
    {
    	return mContext!=null;
    }
    /** 
     * 儲存錯誤資訊到檔案中 
     * @param ex 
     * @return 
     * @throws IOException 
     */  
	private String saveExceptionToFile(Throwable ex,String fileName) 
	{  
		File saveFile = null;
		PrintWriter printWriter = null;  
            try {
            	lock.tryLock();
				if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
				{
					File sdCardDir = Environment.getExternalStorageDirectory();//獲取SDCard目錄
				    saveFile = new File(sdCardDir, fileName);   
				}else{
					 saveFile =new File(mContext.getFilesDir(),fileName);
				}
				if(!saveFile.exists())
			    {
			    	saveFile.createNewFile();
			    }
			    printWriter = new PrintWriter(saveFile);
			    String result = formatException(ex);
			    printWriter.write(result);
			    printWriter.flush();
				Log.e("CrashException", result);
            }catch(Exception e){
				e.printStackTrace();
			} finally{
				if(printWriter!=null)
				{
					printWriter.close();
				}
				lock.unlock();
			}
           
            return saveFile!=null?saveFile.getAbsolutePath():null;  
    }  
    /**
     * 格式化異常資訊
     * @param e
     * @return
     */
    @SuppressLint("SimpleDateFormat")
	private  String  formatException(Throwable e)
	{
    	StringBuilder sb = new StringBuilder();
		StackTraceElement[] stackTrace = e.getStackTrace();
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		if (stackTrace!=null)
		{
			String  timeStramp =  sdf.format(new Date(System.currentTimeMillis()));
			String format = String.format("DateTime:%s\nExceptionName:%s\n\n",timeStramp,e.getLocalizedMessage());
			sb.append(format);
			for (int i = 0; i < stackTrace.length; i++) 
			{
				StackTraceElement traceElement = stackTrace[i];
				String 	fileName 	= traceElement.getFileName();
				int 	lineNumber 		= traceElement.getLineNumber();
				String 	methodName 	= traceElement.getMethodName();
				String 	className = traceElement.getClassName();
				sb.append(String.format("%s\t%s[%d].%s \n",className,fileName,lineNumber,methodName));
			}
			sb.append(String.format("\n%s",e.getMessage()));
			Writer stringWriter = new StringWriter();
			PrintWriter pw = new PrintWriter(stringWriter);
			e.printStackTrace(pw);
			pw.flush();
			pw.close();
			
			sb.append("\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
			sb.append(stringWriter.toString());
		}
		return sb.toString();
	}
}

這裡只儲存了檔案,一般來說,當app第二次啟動時我們需要將該檔案上傳到網路,時間不是很充裕,這裡上傳暫時不貼程式碼了,時間充裕的話會及時補充,請保持關注吧

2.初始化,監聽全域性異常資訊,這裡需要繼承Application,並替換系統預設的Application

public class BaseApplication extends Application 
{
	private  static BaseApplication instance = null;
	private AppCrashHandler appCrashHandler = null;
	@Override
	public void onCreate() {
		synchronized (this)
		{
			if(instance==null)
			{
				instance = this;
			}
			appCrashHandler =  AppCrashHandler.shareInstance(instance);
		}
		super.onCreate();	
	}
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
		super.onConfigurationChanged(newConfig);
	}	
}

修改清單檔案

<application
        android:name="com.hali.luya.unitest.BaseApplication "
        android:hardwareAccelerated="true"
        android:icon="@drawable/app_logo"
        android:logo="@drawable/app_logo"
        android:label="@string/app_name"
        android:configChanges="locale|keyboard|screenSize"
        android:theme="@style/Theme.AppBaseTheme" >       
 <!--- ..這裡省略一大堆程式碼.. ---->
 </Application>



相關文章