1. 程式人生 > >Java 介面的加深理解

Java 介面的加深理解

Java 介面的加深理解

bug 處理– 網路緩慢,網路未請求完資料後 ,關閉 當前 activity,系統崩潰

專案對使用 4G網路和 公司wifi 做了處理,有的介面 在 Android app 端是可以訪問的,有的則不能。 昨天專案上線前測試的時候 發現,系統崩潰,搞得我好尷尬。因為點選 我的功能介面。不管這麼樣,自己的bug, 自己 哭著也要解決。

  1. 確定問題點,首先要問題重現

    先找 找到 bug 的妹子,問下 點選的順序及 過程。

    發現就是 崩潰,好尷尬。。

    又出現問題 ,系統 掛了的時候,什麼都沒有了,什麼 地方出現 bug 也不知道,又是 很尷尬。。

    還是 同事給力,提供了 bug 蒐集 功能 的 工具,==該工具見最後的目錄,可以在 Application 裡面進行初始化操作==

    該工具 可以在 工具裡面打斷點,可以檢視 問題點。

    該問題 報 在 我的 CustomerManagerActivity 類裡面 ,dialog 展示 dialog.show()的時候 依附的 類找不到。 問題點的位置 在 Rxjava 和 Retrofit 的網路 框架裡面的 onError 裡面。

  2. 拿到問題後 就 分析一下 原因:

首先 我有 幾個疑問?

1. 我在 返回的時候 ,關閉了 CustomerManagerActivity 這個類,對他進行 finish 操作 ,為什麼後來 還會 呼叫 retrofit 的 網路請求 的  onError 方法, 畢竟 網路請求 是依附於 Acticity 的?
2. 我的 dialog  也是 依附於 我的 CustomerManagerActivity 這個類,為什麼 他 這個物件還會 存在 ,不會回收?
3. 他們 兩之間 有什麼關係 呢? 都沒有 回收?
4. 為什麼 dialog .show 報錯 ,而不是其他報錯?
5. 如果網路請求 的方法中在 onsuccess 和 onComplated 方法中,其他地方會不會 也會報錯?
6. ***為什麼retrofit介面回撥的方法中 介面回撥完後,記憶體就釋放了?***
  1. 有問題就 解決問題 ,和同事 討論後 感覺 頭腦清晰了 很多。

    1. 網路請求 肯定會 開執行緒去請求 ,只不過 結果可以 通過兩種 形式返回 ,常見的是通過 handler 來進行 處理,還有就是 通過 介面回撥 來 處理。 Rxjava 和 Retrofit 可以理解為 通過 介面 回撥來處理內容。此時的 retrofit 物件為單例模式 ,可能其他地方也在用,沒有 釋放引用物件,所以該 物件還在。
    2. dialog 實在 activity 中初始化的,但是 其 在介面回撥的 物件中被持有,所以 也是不會釋放的。此時不會為空
    3. 正式由於 他們之間 有 持有 物件引用,該物件就不會 在 記憶體中釋放,垃圾回收機制 RC 也不會回收。
    4. 因為 dialog ,popwindow 等是 依附於 內容提供者 Context ,在這裡就是 Activity ,其是 一種浮層,在 Actictiy 上面展示,所以 在 初始化 dialog 等物件 的時候 需要傳入 Context 上下文。如果 當前的Activity 銷燬,其依附的物件 消失,就會報狀態異常
      View=com.android.internal.policy.PhoneWindow$DecorView{..}not attached to window manager,系統就崩潰。
    5. 其他 如果不需要依附Context (acticity )的內容,不展示效果,其處理完後 就會釋放記憶體。也就是還會走介面回撥的方法。但是 走完就走完了。
    6. 糊塗點的 解釋 是用完了之後 ,垃圾回收機制就會釋放記憶體,因為記憶體不足。 稍微清醒些的解釋是:
      由於 retrofit 持有 觀察者和被觀察者的 父類引用,其向下轉型,
      當回撥方法走完後,方法內部的 內容就會 被釋放?。
  2. 發現問題 ,瞭解問題原因後就 剩下解決問題:

    1. dialog 釋放,當 acticity finish 掉後,就會 將 dialog=null,釋放堆記憶體。在 onError 中 對 dialog 判空,如果不為空,再進行展示。
    2. 同事的方案: 考慮 讓 網頁點選返回的時候, 網路請求 中斷。(好像用shutdown ?) 網上有人說 ,在網路請求 父類中 對Context 判空,如果為空,則return ,這種可以應用在 非 單例模式 裡面。
1 為什麼 我介面 呼叫方法名 例如:a()後,就會 呼叫該 方法的方法體{} 裡面的內容?

答: 因為 我 可以將 方法a() 理解為 一個屬性 ,他 在 記憶體地址中是 持有 一個引用地址 ,這個 引用地址 又持有 方法體的 引用地址, 方法體中 持有 相應的物件的 引用。 所以 可以 呼叫方法就會 執行方法體的內容。

初步理解:

sequenceDiagram
方法a->>方法a的方法體: 持有方法a 方法體的引用
方法a的方法體->>方法a:  與方法a繫結
2. 為什麼 我呼叫該方法體,會一步步執行 程式碼?

猜測是 計算機的底層機制,什麼機制不清楚。。。 有響應鏈麼?

3. 為什麼 我在一個類裡面 新建的類 或者單例 ,當依附的這個類 回收了之後 新建的類或者單例 可能還沒有銷燬?

答: 因為 當我 依附的這個類 回收之後 ,這個類 裡面的屬性 和 方法 的引用 地址會 置空,但是 在 其他地方 可能還會持有 該 類的引用,比如說 介面回撥和 單例模式的時候。 也就是說 這個物件 垃圾回收機制是不會回收的,同樣的 方法 沒有釋放,方法體 裡面持有的 物件 也不會釋放。

5. 延伸: 如果我在 子執行緒中 處理介面回撥的邏輯,介面回撥 會馬上執行麼?

答: 不會 。因為 cpu 在執行 執行緒佇列的時候,是按順序走的, 他會回撥到子執行緒,但是 子執行緒什麼時候 執行 就是有 執行緒佇列 裡面的順序決定的。

4. 一些理解的內容:
    1. 類裡面的 屬性 和 方法 都可以理解 為 屬性 ,其作為 一個 引用地址 存在 類 裡面的集合裡,當 用到的時候 去呼叫 就可以了。
    1. 面向過程和麵向物件 怎麼這個老生常談,因為他很重要。面上理解 :面向物件是 對 面向過程的封裝,也就是 將 過程封裝。 再粗點理解 可以將 方法和方法體 理解為 一個物件—- 本來就是一個物件







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());
}

}

“`