Android優化系列一:日誌清理

snowdream86發表於2016-10-24

簡介

在Android應用開發過程中,通過Log類輸出日誌是一種很重要的除錯手段。
大家對於Log類的使用,一般會形成幾點共識:

  1. 在Debug模式下列印日誌,在Release模式下不列印日誌
  2. 避免濫用Log類進行輸出日誌。因為這樣可能造成日誌刷屏,淹沒真正有用的日誌。
  3. 封裝Log類,以提供同時輸出日誌到檔案等功能

具體細化為以下幾點建議:

  1. 禁用System.out.println
    Android應用中,一般通過封裝過的Log類來輸出日誌,方便控制。而System.out.println是標準的Java輸出方法,使用不當,可能造成Release模式下輸出日誌的結果。
  2. 禁用e.printStackTrace
    禁用理由同上

建議通過封裝過的Log類來輸出異常堆疊資訊

  1. Debug模式下,通過一個靜態變數,控制日誌的顯示隱藏。
    我一般習慣直接使用BuildConfig.DEBUG,當然,你也可以自己定義一個。

private static final boolean isDebug = BuildConfig.DEBUG;

public static void i(String tag, String msg) {
    if (isDebug) {
        Log.i(tag, msg);
    }
}

4.Release模式下,通過Proguard配置來移除日誌
在Proguard配置檔案中,確保沒有新增 –dontoptimize選項 來禁用優化的前提下,
新增以下程式碼:

-assumenosideeffects class android.util.Log {
        public static *** d(...);
        public static *** e(...);
        public static *** i(...);
        public static *** v(...);
        public static *** println(...);
        public static *** w(...);
        public static *** wtf(...);
}

那麼,是否我們按照上面的做,就真的一勞永逸呢?
我的腦海中浮現出幾個相關問題:
1.Proguard配置中新增的配置,真的可以在Release模式下,移除日誌嗎?
2.如果我們用的是封裝過的Log工具類,應該怎麼配置?
3.移除日誌後,原來在日誌方法中的拼接字串引數,是否還會申請/佔用記憶體?

本著大膽假設,小心求證的原則,下面我們通過實踐來探索上面的問題答案。

本文基於以下專案進行測試實踐:
https://github.com/snowdream/test/tree/master/android/test/logtest
反編譯工具:JD-GUI

驗證Proguard配置清理日誌的有效性

CASE

Log.i(TAG,"這樣使用,得到的LOGTAG的值就是DroidSettings," +
        "然而並非如此,當DroidSettings這個類進行了混淆之後,類名變成了類似a,b,c這樣的名稱," +
        "LOGTAG則不再是DroidSettings這個值了。這樣可能造成的問題就是,內部混淆有日誌的包,我們去過濾DroidSettings " +
        "卻永遠得不到任何資訊。");

在新增上述Proguard配置前後,編譯打包Release模式的正式包,使用JD-GUI進行反編譯,對比上述程式碼的編譯後程式碼。

結果

新增配置前
case1_a

新增配置後
case1_b

結論

通過比對結果,我們可以得出結論:
通過新增Proguard配置,可以在Release模式下,移除掉日誌。

驗證封裝過的Log工具類,是否有必要進行而外配置

CASE

LogUtil.i(TAG,"這樣使用,得到的LOGTAG的值就是DroidSettings," +
        "然而並非如此,當DroidSettings這個類進行了混淆之後,類名變成了類似a,b,c這樣的名稱," +
        "LOGTAG則不再是DroidSettings這個值了。這樣可能造成的問題就是,內部混淆有日誌的包,我們去過濾DroidSettings " +
        "卻永遠得不到任何資訊。");
        

在新增上述Proguard配置前後,編譯打包Release模式的正式包,使用JD-GUI進行反編譯,對比上述程式碼的編譯後程式碼。

結果

新增配置前
case2_a

新增配置後
case2_b

結論

通過比對結果,我們可以得出結論:
在這種簡單封裝的情況下,我們不需要額外的配置,也可以將封裝過的Log工具類呼叫日誌一起移除。

當然,實際使用過程中,可能封裝更復雜。為了保險起見,可以也新增上Log工具類的配置。示例如下:

-assumenosideeffects class com.github.snowdream.logtest.LogUtil {
        public static *** d(...);
        public static *** e(...);
        public static *** i(...);
        public static *** v(...);
        public static *** w(...);
}

驗證移除日誌後,字串拼接是否還存在?

CASE

Log.i(TAG,"這樣使用,得到的LOGTAG的值就是DroidSettings," +
        "然而並非如此,當DroidSettings這個類進行了混淆之後,類名變成了類似a,b,c這樣的名稱," +
        "LOGTAG則不再是DroidSettings這個值了。這樣可能造成的問題就是,內部混淆有日誌的包,我們去過濾DroidSettings " +
        "卻永遠得不到任何資訊。");

Log.i(TAG, "這樣使用,得到的LOGTAG的值就是DroidSettings," +
        "然而並非如此,當DroidSettings這個類進行了混淆之後,類名變成了類似a,b,c這樣的名稱," +
        "LOGTAG則不再是DroidSettings這個值了。這樣可能造成的問題就是,內部混淆有日誌的包,我們去過濾DroidSettings " +
        "卻永遠得不到任何資訊。" + index ++);
        

上面程式碼的區別是:
前面是簡單的字串相加,而後面是字串和變數的相加
在新增上述Proguard配置的前提下,分別針對以上兩段程式碼,編譯打包Release模式的正式包,使用JD-GUI進行反編譯,對比上述程式碼的編譯後程式碼。

結果

簡單字串相加
case1_b
字串和變數相加
case3

結論

通過比對結果,我們可以得出結論:
如果只是簡單字串相加,是會徹底移除的,並且字串拼接也不見了,不會佔用記憶體。
而如果是字串和變數相加,日誌會移除,但是字串拼接還在,還會佔用記憶體。

驗證日誌中使用函返回值的情況

CASE


        LogUtil.i(TAG, getMessage());

        LogUtil.i(TAG, "FROM FUNCTION " + getMessage());
        
        private String getMessage() {
            return  "這樣使用,得到的LOGTAG的值就是DroidSettings," +
                "然而並非如此,當DroidSettings這個類進行了混淆之後,類名變成了類似a,b,c這樣的名稱," +
                "LOGTAG則不再是DroidSettings這個值了。這樣可能造成的問題就是,內部混淆有日誌的包,我們去過濾DroidSettings " +
                "卻永遠得不到任何資訊。";
        }
        

上面程式碼的區別是:
前面是直接使用函式返回值,而後面是字串和函式返回值的相加
在新增上述Proguard配置的前提下,分別針對以上兩段程式碼,編譯打包Release模式的正式包,使用JD-GUI進行反編譯,對比上述程式碼的編譯後程式碼。

結果

直接使用函式返回值
case1_b

字串和函式返回值相加
case4

結論

通過比對結果,我們可以得出結論:
以上兩種場景下,日誌移除,拼接字串不在了,也不會佔用記憶體。

經過大量實踐後的結論

如果你以為上面就是全部真相的話,就錯了。
經過大量的測試實踐,實際上真相更復雜。

以下是開啟Proguard前提下,各種情況下的測試結論:

  1. Log.i(簡單字串)
  2. Log.i(區域性變數)
  3. Log.i(成員變數)
  4. Log.i(簡單字串+區域性變數)
    以上四種情況,日誌被徹底移除,不會額外增加記憶體。
  5. Log.i(簡單字串+成員變數)
    日誌被移除,但是字串拼接會存在,並佔用記憶體。
  6. Log.i(成員函式) 其中,成員函式返回值為: 簡單字串
  7. Log.i(成員函式) 其中,成員函式返回值為: 簡單字串+區域性變數
    以上兩種情況,日誌被徹底移除,不會額外增加記憶體。
  8. Log.i(成員函式) 其中,成員函式返回值為: 簡單字串+成員變數
    日誌被移除,但是字串拼接會存在,並佔用記憶體。

注:以上所有情況,引數都是指第二個或者後面的引數。第一個引數,我都使用了靜態成員變數:
private static final String TAG = MainActivity.class.getSimpleName();

優化建議

1.確保沒有開啟 –dontoptimize選項的前提下,新增Proguard優化日誌配置

-assumenosideeffects class android.util.Log {
        public static *** d(...);
        public static *** e(...);
        public static *** i(...);
        public static *** v(...);
        public static *** println(...);
        public static *** w(...);
        public static *** wtf(...);
}

2.針對這種情況“Log.i(成員函式) 其中,成員函式返回值為: 簡單字串+成員變數”
目前並沒有辦法規避,不建議這麼使用。
3.針對這種情況”Log.i(簡單字串+成員變數)”
我們的解決方案是,在封裝的Log工具類方法中,使用變長引數。
下面是一個簡單的示例:

package com.github.snowdream.logtest;

import android.text.TextUtils;
import android.util.Log;

/**
 * Created by snowdream on 16-10-22.
 */
public class LogUtil {
    private static final boolean isDebug = BuildConfig.DEBUG;

    public static void i(String tag, String... args) {
        if (isDebug) {
            Log.i(tag, getLog(tag,args));
        }
    }

    public static void d(String tag, String... args) {
        if (isDebug) {
            Log.i(tag, getLog(tag,args));
        }
    }

    public static void v(String tag, String... args) {
        if (isDebug) {
            Log.i(tag, getLog(tag,args));
        }
    }

    public static void w(String tag, String... args) {
        if (isDebug) {
            Log.i(tag, getLog(tag,args));
        }
    }

    public static void e(String tag, String... args) {
        if (isDebug) {
            Log.i(tag, getLog(tag,args));
        }
    }

    private static String getLog(String tag, String... args){
        StringBuilder builder = new StringBuilder();
        for (String arg : args){
            if (TextUtils.isEmpty(arg)) continue;

            builder.append(arg);
        }

        return builder.toString();
    }
}

參考

1.如何安全地列印日誌
2.關於Android Log的一些思考
3.Androrid應用打包release版時關閉log日誌輸出


相關文章