Java 介面的加深理解
Java 介面的加深理解
bug 處理– 網路緩慢,網路未請求完資料後 ,關閉 當前 activity,系統崩潰
專案對使用 4G網路和 公司wifi 做了處理,有的介面 在 Android app 端是可以訪問的,有的則不能。 昨天專案上線前測試的時候 發現,系統崩潰,搞得我好尷尬。因為點選 我的功能介面。不管這麼樣,自己的bug, 自己 哭著也要解決。
確定問題點,首先要問題重現
先找 找到 bug 的妹子,問下 點選的順序及 過程。
發現就是 崩潰,好尷尬。。
又出現問題 ,系統 掛了的時候,什麼都沒有了,什麼 地方出現 bug 也不知道,又是 很尷尬。。
還是 同事給力,提供了 bug 蒐集 功能 的 工具,==該工具見最後的目錄,可以在 Application 裡面進行初始化操作==
該工具 可以在 工具裡面打斷點,可以檢視 問題點。
該問題 報 在 我的 CustomerManagerActivity 類裡面 ,dialog 展示 dialog.show()的時候 依附的 類找不到。 問題點的位置 在 Rxjava 和 Retrofit 的網路 框架裡面的 onError 裡面。
拿到問題後 就 分析一下 原因:
首先 我有 幾個疑問?
1. 我在 返回的時候 ,關閉了 CustomerManagerActivity 這個類,對他進行 finish 操作 ,為什麼後來 還會 呼叫 retrofit 的 網路請求 的 onError 方法, 畢竟 網路請求 是依附於 Acticity 的? 2. 我的 dialog 也是 依附於 我的 CustomerManagerActivity 這個類,為什麼 他 這個物件還會 存在 ,不會回收? 3. 他們 兩之間 有什麼關係 呢? 都沒有 回收? 4. 為什麼 dialog .show 報錯 ,而不是其他報錯? 5. 如果網路請求 的方法中在 onsuccess 和 onComplated 方法中,其他地方會不會 也會報錯? 6. ***為什麼retrofit介面回撥的方法中 介面回撥完後,記憶體就釋放了?***
有問題就 解決問題 ,和同事 討論後 感覺 頭腦清晰了 很多。
- 網路請求 肯定會 開執行緒去請求 ,只不過 結果可以 通過兩種 形式返回 ,常見的是通過 handler 來進行 處理,還有就是 通過 介面回撥 來 處理。 Rxjava 和 Retrofit 可以理解為 通過 介面 回撥來處理內容。此時的 retrofit 物件為單例模式 ,可能其他地方也在用,沒有 釋放引用物件,所以該 物件還在。
- dialog 實在 activity 中初始化的,但是 其 在介面回撥的 物件中被持有,所以 也是不會釋放的。此時不會為空
- 正式由於 他們之間 有 持有 物件引用,該物件就不會 在 記憶體中釋放,垃圾回收機制 RC 也不會回收。
- 因為 dialog ,popwindow 等是 依附於 內容提供者 Context ,在這裡就是 Activity ,其是 一種浮層,在 Actictiy 上面展示,所以 在 初始化 dialog 等物件 的時候 需要傳入 Context 上下文。如果 當前的Activity 銷燬,其依附的物件 消失,就會報狀態異常
View=com.android.internal.policy.PhoneWindow$DecorView{..}not attached to window manager,系統就崩潰。 - 其他 如果不需要依附Context (acticity )的內容,不展示效果,其處理完後 就會釋放記憶體。也就是還會走介面回撥的方法。但是 走完就走完了。
- 糊塗點的 解釋 是用完了之後 ,垃圾回收機制就會釋放記憶體,因為記憶體不足。 稍微清醒些的解釋是:
由於 retrofit 持有 觀察者和被觀察者的 父類引用,其向下轉型,
當回撥方法走完後,方法內部的 內容就會 被釋放?。
發現問題 ,瞭解問題原因後就 剩下解決問題:
- dialog 釋放,當 acticity finish 掉後,就會 將 dialog=null,釋放堆記憶體。在 onError 中 對 dialog 判空,如果不為空,再進行展示。
- 同事的方案: 考慮 讓 網頁點選返回的時候, 網路請求 中斷。(好像用shutdown ?) 網上有人說 ,在網路請求 父類中 對Context 判空,如果為空,則return ,這種可以應用在 非 單例模式 裡面。
1 為什麼 我介面 呼叫方法名 例如:a()後,就會 呼叫該 方法的方法體{} 裡面的內容?
答: 因為 我 可以將 方法a() 理解為 一個屬性 ,他 在 記憶體地址中是 持有 一個引用地址 ,這個 引用地址 又持有 方法體的 引用地址, 方法體中 持有 相應的物件的 引用。 所以 可以 呼叫方法就會 執行方法體的內容。
初步理解:
sequenceDiagram
方法a->>方法a的方法體: 持有方法a 方法體的引用
方法a的方法體->>方法a: 與方法a繫結
2. 為什麼 我呼叫該方法體,會一步步執行 程式碼?
猜測是 計算機的底層機制,什麼機制不清楚。。。 有響應鏈麼?
3. 為什麼 我在一個類裡面 新建的類 或者單例 ,當依附的這個類 回收了之後 新建的類或者單例 可能還沒有銷燬?
答: 因為 當我 依附的這個類 回收之後 ,這個類 裡面的屬性 和 方法 的引用 地址會 置空,但是 在 其他地方 可能還會持有 該 類的引用,比如說 介面回撥和 單例模式的時候。 也就是說 這個物件 垃圾回收機制是不會回收的,同樣的 方法 沒有釋放,方法體 裡面持有的 物件 也不會釋放。
5. 延伸: 如果我在 子執行緒中 處理介面回撥的邏輯,介面回撥 會馬上執行麼?
答: 不會 。因為 cpu 在執行 執行緒佇列的時候,是按順序走的, 他會回撥到子執行緒,但是 子執行緒什麼時候 執行 就是有 執行緒佇列 裡面的順序決定的。
4. 一些理解的內容:
-
- 類裡面的 屬性 和 方法 都可以理解 為 屬性 ,其作為 一個 引用地址 存在 類 裡面的集合裡,當 用到的時候 去呼叫 就可以了。
-
- 面向過程和麵向物件 怎麼這個老生常談,因為他很重要。面上理解 :面向物件是 對 面向過程的封裝,也就是 將 過程封裝。 再粗點理解 可以將 方法和方法體 理解為 一個物件—- 本來就是一個物件
package com.taikanglife.isalessystem.crash;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* Created by QunCheung on 2017/1/22.
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
//系統預設的UncaughtException處理類
private Thread.UncaughtExceptionHandler mDefaultHandler;
//CrashHandler例項
private static CrashHandler INSTANCE;
//程式的Context物件
private Context mContext;
//用來儲存裝置資訊和異常資訊
private Map<String, String> infos = new HashMap<String, String>();
//檔案價名稱
private static final String TAG = "crash";
//檔名稱
private static final String FILE_NAME = "crashlog";
//檔案字尾
private static final String FORMAT = ".txt";
//儲存位置資料夾
private String logdir;
//根據時間,定期上傳日誌
public final static int TIME = 1;
//根據日誌大小,上傳日誌
public final static int MEMORY = 2;
//根據,時間,日誌大小,上傳日誌
public final static int TIME_AND_MEMORY = 3;
//type
private int TYPE = 3;
//first init
private boolean isFirstInit;
//first init string
private final static String IS_FIRST_INIT = "is_first_init";
//first init time
private final static String FIRST_INIT_TIME = "first_init_time";
//時間差
private int TIME_SIZE;
//設定的時間間隔
private int DAYS = 7;
private CrashHandler() {
}
/**
* 獲取CrashHandler例項 ,單例模式
*/
public static CrashHandler getInstance() {
if (INSTANCE == null)
INSTANCE = new CrashHandler();
return INSTANCE;
}
/**
* 初始化
*
* @param context
* @param type 上傳模式
*/
public void init(Context context, int type) {
mContext = context;
//獲取系統預設的UncaughtException處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//設定該CrashHandler為程式的預設處理器
Thread.setDefaultUncaughtExceptionHandler(this);
//設定檔案儲存路徑
logdir = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + TAG;
this.TYPE = type;
//時間儲存
saveOrGetTimeType();
}
/**
* 初始化
*
* @param context 預設按照日誌大小,控制日誌上傳
*/
public void init(Context context) {
mContext = context;
//獲取系統預設的UncaughtException處理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//設定該CrashHandler為程式的預設處理器
Thread.setDefaultUncaughtExceptionHandler(this);
//設定檔案儲存路徑
logdir = Environment.getExternalStorageDirectory().getAbsolutePath()
+ File.separator + "crash_log";
this.TYPE = 3;
//時間儲存
saveOrGetTimeType();
}
/**
* 獲取儲存時間資訊
*/
private void saveOrGetTimeType() {
SharedPreferences crash = mContext.getSharedPreferences("crash", 1);
isFirstInit = crash.getBoolean(IS_FIRST_INIT, true);
if (isFirstInit) {
SharedPreferences.Editor edit = crash.edit();
edit.putBoolean(IS_FIRST_INIT, false);
edit.putLong(FIRST_INIT_TIME, getTimeToLong());
edit.commit();
} else {
long firstTime = crash.getLong(FIRST_INIT_TIME, 0);
long currentTime = getTimeToLong();
TIME_SIZE = (int) ((currentTime - firstTime) / 86400);
}
}
/**
* 獲取時間
*/
private long getTimeToLong() {
try {
long time = Calendar.getInstance().getTimeInMillis();
return time;
} catch (Exception e) {
return 0;
}
}
/**
* 當UncaughtException發生時會轉入該函式來處理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
//如果使用者沒有處理則讓系統預設的異常處理器來處理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Log.i(TAG, "uncaughtException: " + e.toString());
}
//退出程式
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
}
/**
* 自定義錯誤處理,收集錯誤資訊 傳送錯誤報告等操作均在此完成.
*
* @param ex
* @return true:如果處理了該異常資訊;否則返回false.
*/
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);
//儲存日誌檔案
saveCrashInfoToFile(ex);
return true;
}
/**
* 並不應該每次崩潰都進行日誌上傳
*
* @param file
*/
private void svaeCrashInfoToServer(File file) {
// TODO: 2017/1/22 文字大小過大,必須上傳
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "日誌已經很大了,應該上傳伺服器", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
}
/**
* 收集裝置引數資訊
*
* @param ctx
*/
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.i(TAG, "collectDeviceInfo: " + e.toString());
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
} catch (Exception e) {
Log.i(TAG, "collectDeviceInfo: " + e.toString());
}
}
}
/**
* 儲存錯誤資訊到檔案中,應該清楚過長時間的錯誤資訊
*
* @param ex
* @return 返回檔名稱, 便於將檔案傳送到伺服器
*/
private String saveCrashInfoToFile(Throwable ex) {
StringBuffer sb = new StringBuffer();
Date date = Calendar.getInstance().getTime();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String times = sdf.format(date);
sb.append("---------------------sta--------------------------");
sb.append("crash at time: " + times);
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);
//可以追加使用者資訊
sb.append(addUserInfo());
sb.append("--------------------end---------------------------");
File file = getFilePath(logdir, FILE_NAME + FORMAT);
switch (TYPE) {
case 1:
case 2:
if (checkIsTimeToPush()) {
//儲存日誌到伺服器
svaeCrashInfoToServer(file);
}
case 3:
//檢查日誌是否過大
if (checkFileIsToBig(file)) {
//儲存日誌到伺服器
svaeCrashInfoToServer(file);
//寫入本地,清空文字
// TODO: 2017/2/27 如果寫本地則開啟註釋
WriteContentTypt(file,sb,true);
} else {
//寫入本地,追加文字
WriteContentTypt(file,sb,false);
}
break;
default:
break;
}
//放開以上註釋,請刪除此行
// Log.i(TAG, “WriteContentTypt: ” + sb.toString());
return null;
}
/**
* 按照時間週期上傳日誌
*/
private boolean checkIsTimeToPush() {
if (TIME_SIZE >= DAYS) {
return true;
} else {
return false;
}
}
private String addUserInfo() {
return "UserInfo:\n";
}
private boolean checkFileIsToBig(File file) {
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
int fileSize = fis.available();
if (fileSize > 1024 * 1024 * 5) {
return true;
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
/**
* 建立檔案
*
* @param filePath 檔案路徑
* @param fileName 檔名稱
* @return
*/
public static File getFilePath(String filePath,
String fileName) {
File file = new File(filePath, fileName);
if (file.exists()) {
return file;
} else {
file = null;
makeRootDirectory(filePath);
try {
file = new File(filePath + File.separator + fileName);
} catch (Exception e) {
// Auto-generated catch block
e.printStackTrace();
}
return file;
}
}
/**
* 建立根目錄
*
* @param filePath
*/
public static void makeRootDirectory(String filePath) {
File file = null;
try {
file = new File(filePath);
if (!file.exists()) {
file.mkdir();
}
} catch (Exception e) {
}
}
/**
* 寫入本地檔案
*
* @param file 檔案
* @param sb 內容
* @param isClean 是否清空,true:清空,false:保留
*/
public void WriteContentTypt(File file, StringBuffer sb, boolean isClean) {
try {
FileOutputStream fos = new FileOutputStream(file, !isClean);
fos.flush();
for (char c : sb.toString().toCharArray()) {
fos.write(c);
}
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
Log.i(TAG, "WriteContentTypt: " + sb.toString());
}
}
“`