編輯:關於Android編程
Android應用出現crash時,會出現“程序異常退出”的提示,隨後應用關閉,用戶體驗非常不好。一般為了捕獲應用運行時異常後做出適當處理,給出合理提示時,我們開發中可以繼承UncaughtExceptionHandler類來處理,提升用戶體驗。
(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; /** * ** <功能詳細描述> * * @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 infos = new HashMap (); 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 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 activitys = new ArrayList(); /** * 加解密密鑰 */ 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) { ArrayListfiles = new ArrayList (); for (File file : listFiles) { files.add(file); } Map > fileParameters = new HashMap >(); 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 ""; } }
如果您通過以下的代碼來獲取定義的顏色值context.getResources().getColor(R.color.some_color_resource_id);在
當我們拿到一台Android的智能手機,從打開開關,到我們可以使用其中的app時,這個啟動過程到底是怎麼樣的? 系統上電 當給Android系統上電,CPU復位之後,
Android WebView 1.首先修改activity.xml中的代碼:2.然後MainActivity中的代碼:3.最後設置權限:<uses-permiss
限於篇幅的原因,在上篇文章中我們只學習了ActionBar基礎部分的知識,那麼本篇文章我們將接著上一章的內容繼續學習,探究一下ActionBar更加高級的知