用Proguard的-assumenosideeffects清除log


转自:用Proguard的-assumenosideeffects清除log


在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类来输出异常堆栈信息

  3. 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进行反编译,对比上述代码的编译后代码。

结果

添加配置前



添加配置后




结论

通过比对结果,我们可以得出结论:
通过添加Proguard配置,可以在Release模式下,移除掉日志。


验证封装过的Log工具类,是否有必要进行而外配置

CASE

LogUtil.i(TAG,"这样使用,得到的LOGTAG的值就是DroidSettings," 
+ "然而并非如此,当DroidSettings这个类进行了混淆之后,类名变成了类似a,b,c这样的名称,"
+ "LOGTAG则不再是DroidSettings这个值了。这样可能造成的问题就是,内部混淆有日志的包,我们去过滤DroidSettings "
+"却永远得不到任何信息。");

在添加上述Proguard配置前后,编译打包Release模式的正式包,使用JD-GUI进行反编译,对比上述代码的编译后代码。

结果

添加配置前



添加配置后




结论

通过比对结果,我们可以得出结论:
在这种简单封装的情况下,我们不需要额外的配置,也可以将封装过的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进行反编译,对比上述代码的编译后代码。

结果

简单字符串相加


 

字符串和变量相加


 

结论

通过比对结果,我们可以得出结论:
如果只是简单字符串相加,是会彻底移除的,并且字符串拼接也不见了,不会占用内存。
而如果是字符串和变量相加,日志会移除,但是字符串拼接还在,还会占用内存。

验证日志中使用函返回值的情况

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.png

字符串和函数返回值相加


case4.png

结论

通过比对结果,我们可以得出结论:
以上两种场景下,日志移除,拼接字符串不在了,也不会占用内存。

经过大量实践后的结论

如果你以为上面就是全部真相的话,就错了。
经过大量的测试实践,实际上真相更复杂。

以下是开启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();    } }
------------------------------------------------------ buildConfigField的巧妙应用------------------------------------------

当用AndroidStudio来进行Android项目开发时,build.gradle就是这个工具的核心部分,所有的依赖,debug/release设置,混淆等都在这里进行配置。 
下面就主要来记录下利用buildConfigField来为我们的项目进行动态配置的目的 
eg:debug:打印日志,在内网测试,利用签名 
release:关闭日志,外网,签名等 
先贴出一个完事的build.gradle文件,如下:

apply plugin: 'com.android.application'
apply plugin: 'AndResGuard'//利用微信的资源混淆工具
//apply plugin: 'io.fabric'

buildscript {
    repositories {
        jcenter()
        maven { url 'https://maven.fabric.io/public' }
    }
    dependencies {
        // The Fabric Gradle plugin uses an open ended version to react
        // quickly to Android tooling updates
        classpath 'io.fabric.tools:gradle:latest.integration'
    }
}

repositories {
    jcenter()
    maven { url 'https://maven.fabric.io/public' }
}
android {
    compileSdkVersion 21
    buildToolsVersion "22.0.1"

    packagingOptions {
        exclude 'LICENSE.txt'
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/dependencies'
        exclude 'META-INF/DEPENDENCIES.txt'
        exclude 'META-INF/dependencies.txt'
        exclude 'META-INF/LGPL2.1'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/license'
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/license.txt'
        exclude 'META-INF/NOTICE'
        exclude 'META-INF/notice'
        exclude 'META-INF/NOTICE.txt'
        exclude 'META-INF/notice.txt'
        exclude 'META-INF/README.txt'
        exclude 'META-INF/services/javax.annotation.processing.Processor'
        exclude '!META-INF/MANIFEST.MF'
        exclude 'META-INF/MANIFEST.MF'

    }


    defaultConfig {
        targetSdkVersion 21
        // multiDexEnabled true
    }

    /**
     * 这前用上面multiDexEnabled true时,
     * 突然在5.0以下的手机上跑不起来,
     * 改成下面这种写法就可以了。
     */
    dexOptions {
        jumboMode true
    }

    signingConfigs {
        release {
            try {
                storeFile file("../keystore/Het_KeyStore.keystore")//这里替换成你自己项目生成的keystore的存储路径
                storePassword "szhittech"
                keyAlias "Clife"
                keyPassword "szhittechclife"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }

        debug {
            try {
                storeFile file("../keystore/Het_KeyStore.keystore")
                storePassword "szhittech"
                keyAlias "Clife"
                keyPassword "szhittechclife"
            } catch (ex) {
                throw new InvalidUserDataException(ex.toString())
            }
        }
    }

    buildTypes {
        release {
 buildConfigField("boolean", "LOG_DEBUG", "false")
            buildConfigField "String", "SERVER_HOST", "\"api.clife.cn/\""
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            //是否清理无用资源
            shrinkResources true
            //是否启用zipAlign压缩
            zipAlignEnabled true
            pseudoLocalesEnabled true
            signingConfig signingConfigs.release
        }

        debug {
         buildConfigField("boolean", "LOG_DEBUG", "true")
            buildConfigField "String", "SERVER_HOST", "\"200.200.200.50/\""
            minifyEnabled true//true:启用混淆,false:不启用
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            shrinkResources false
            zipAlignEnabled true
            pseudoLocalesEnabled true
            signingConfig signingConfigs.release
        }
    }

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            assets.srcDirs = ['assets']
            java.srcDirs = ['src']
            res.srcDirs = ['res']
            jniLibs.srcDirs = ['libs']
        }
    }

    lintOptions {
        abortOnError false

    }

    /* buildTypes{
         debug{
             signingConfig  signingConfigs.HetConfig
         }

         release {
             signingConfig  signingConfigs.HetConfig
         }
     }*/
}
//微信资源混淆工具
andResGuard {
    mappingFile = null
    use7zip = true
    keepRoot = false
    // add <yourpackagename>.R.drawable.icon into whitelist.
    // because the launcher will get the icon with his name
    whiteList = [
            "andresguard.tencent.com.andresguard_example.R.drawable.icon",
            //https://docs.fabric.io/android/crashlytics/build-tools.html
            "andresguard.tencent.com.andresguard_example.R.string.com.crashlytics.*"
    ]
    compressFilePattern = [
            "*.png",
            "*.jpg",
            "*.jpeg",
            "*.gif",
            "resources.arsc"
    ]
    //修改成本地SDK中zipalign路径
    // zipAlignPath = '/Users/sun/Library/Android/sdk/build-tools/21.1.1/zipalign'
    zipAlignPath = ' D:\\eclipse\\all_adt-bundle-windows-x86_64-20140321\\adt-bundle-windows-x86_64-20140321\\sdk\\build-tools\\21.1.1\\zipalign'

    //替换成本地7zip路径
    //sevenZipPath = '/usr/local/bin/7za'
    sevenZipPath = 'D:\\2345Soft\\7zip\\7-Zip\\7z'
}

dependencies {
    compile fileTree(dir: 'compile-libs', include: '*.jar')
    //    provided files('../csleep/libs/butterknife-5.1.2.jar')
    compile fileTree(dir: 'libs', include: '*.jar')
    provided files('../CommonBusiness/build/intermediates/bundles/release/classes.jar')
    // provided files('../CommonResource/build/intermediates/bundles/release/classes.jar')
    compile project(':CLifeAsr')
    compile project(':csleep')
    compile project(':CommonResource')
    provided files('../ShareSDK/libs/libammsdk.jar')
    provided files('../ShareSDK/libs/open_sdk.jar')
    provided files('../CommonBusiness/libs/eventbus.jar')
//    configurations {
//        all*.exclude group: 'com.android.support', module: 'support-v4'
//    }

    compile('com.crashlytics.sdk.android:crashlytics:2.5.2@aar') { transitive = true; }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182

这个build.gradle只是自己项目中用到的,肯定没有包含所有,也没有哪个项目会在build.gralde中把所有东西都用到。这里只想起个抛砖引玉的作用,这里没有的,google上肯定可以查到的。 
下面就只说说build.gradle 中buildTypes中的buildConfigField的作用。 
如上:

 buildConfigField("boolean", "LOG_DEBUG", "true")
            buildConfigField "String", "SERVER_HOST", "\"200.200.200.50/\""
 
 
  • 1
  • 2
  • 1
  • 2

看下groovy中的源码:

 /**
     * Adds a new field to the generated BuildConfig class.
     *
     * <p>The field is generated as: <code>&lt;type&gt; &lt;name&gt; = &lt;value&gt;;</code>
     *
     * <p>This means each of these must have valid Java content. If the type is a String, then the
     * value should include quotes.
     *
     * @param type the type of the field
     * @param name the name of the field
     * @param value the value of the field
     */
    public void buildConfigField(
            @NonNull String type,
            @NonNull String name,
            @NonNull String value) {
        ClassField alreadyPresent = getBuildConfigFields().get(name);
        if (alreadyPresent != null) {
            logger.info(
                    "BuildType(${getName()}): buildConfigField '$name' value is being replaced: ${alreadyPresent.value} -> $value");
        }
        addBuildConfigField(AndroidBuilder.createClassField(type, name, value));
    }

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

一目了然,这个方法接收三个非空的参数,第一个:确定值的类型,第二个:指定key的名字,第三个:传值 
上面的意思是:为LOG_DEBUG = true 
那这个值怎么读取呢?在Groovy中,直接由BuildConfig类点出key名来取值,如下:

if(BuildConfig.LOG_DEBUG){
//Debug,打印日志
    Logger.init("AppPlusLog").setLogLevel(LogLevel.FULL);
}else{
//release,关闭日志
    Logger.init("AppPlusLog").setLogLevel(LogLevel.None);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

同样的,网络切换也可以这样设置

 buildConfigField "String", "SERVER_HOST", "\"200.200.200.50/\""


//取值
String host = BuildConfig.SERVER_HOST;
//然后再把这个host替换掉默认的那个

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值