开发时打印日志,发布的应用不打印日志,这是几乎每个Android项目都会有的需求。有的会写一个工具类,所有日志通过调用工具类的静态方法来打印,在此静态方法里做判断:
import android.util.Log;
public class LogUtils {
public static final boolean IS_LOG_ON = true; // 发布时改为false
public static void log(String tag, String msg) {
if (IS_LOG_ON) {
Log.d(tag, msg);
}
}
}
这种做法有一个问题:在release版本中仍然存在打印日志的开销,如下面的代码所示:
void someMethod(int index){
LogUtils.log("tag", "index: " + index); // 拼了!
...
}
release版本在这里虽然不会打印日志,但是仍然会new一个StringBuilder对象来拼接字符串,这是有开销的,如果在循环或者会被多次调用的方法里打印日志,情况会更糟糕。因此,最好在编译期就彻底删除掉日志打印的调用。iOS开发里用宏可以方便地实现这一点,其实Android也提供了这样的工具:ProGuard。在ProGuard的配置文件里可以声明某些方法没有副作用,在打包release版本时ProGuard会自动帮你删除掉这些方法。这样我们可以直接使用android.util.Log打印日志,然后在ProGuard配置文件中添加下段代码:
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int w(...);
public static int d(...);
public static int e(...);
}
就可以实现release版本无日志。需要注意的是使用此功能必须打开ProGuard的优化开关,具体配置可参考SDK目录下的tools/proguard/proguard-android-optimize.txt文件。
已知最快的办法是美团技术博客的这篇文章介绍的第三种方法。需要注意的是在生成渠道包后需要重新用 zipalign 处理 apk。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(Bundle savedInstanceState);
TextView textView = findViewById(R.id.title);
textView.setText(getString(R.string.title)); // 应使用更简洁的API
textView.setText(R.string.title); // 更易读
StringBuilder sb = new StringBuilder();
sb.append("some text");
sb.append("more text");
textView.setText(sb.toString()); // toString()是多余的
textView.setText(sb); // 更简洁
/*
* 拿到一个Editor,更新一个值
*/
Editor editor = getSharedPreferences("my preference", Context.MODE_PRIVATE)
.edit();
editor.putString("some key", "some value");
editor.commit();
/*
* 使用链式API,更简洁
*/
getSharedPreferences("my preference", Context.MODE_PRIVATE).edit()
.putString("some key", "some value").commit();
}
// 对传入的一个目录做操作,如果目录不存在,需要先创建,下面是繁琐的写法:
public void mkdirIfNeededVerbose(File file) {
boolean flag = true;
if (!file.exists()) {
flag = file.mkdir();
}
if (flag) {
// do file operations
}
}
// 简洁的写法,与繁琐的写法完全等价,但短小易读
public void mkdirIfNeeded(File file) {
if (file.exists() || file.mkdir()) {
// do file operations
}
}
有这么一行代码:
new DecimalFormat("#0.00").format(2.34);
在某些手机上这一行会返回“2,34”,小数点变成了英文逗号。阅读文档发现,直接用new这么创建DecimalFormat对象,会使用系统默认的Locale。进一步搜索发现,在某些国家或地区对应的Locale下,小数点就是应该被格式化为英文逗号,因为在这些国家(如法国)里人们的习惯就是如此。如果要保证格式化后为小数点,必须指定Locale:
DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
df.applyPattern("#0.00");
df.format(2.34);
第一种写法在Lint下不会报警,在大多数手机上也能正确运行。没有类似经验的人很难想到这里不指定Locale会出问题。另一方面,文档强烈地建议了使用指定Locale的方法来创建对象。所以,我们要重视文档。单就Locale这点来说,还有很多方法使用系统默认Locale,这在API文档中一般都有说明。比如:
"i".toUpperCase(); // 使用系统默认Locale,结果可能不是I,比如在土耳其Locale下,结果为İ "i".toUpperCase(Locale.US); // 指定Locale,结果一定是I
如果开发者想要的行为是在任何情况下结果都为I,那么必须指定Locale,一般使用Locale.US,Android保证这个Locale在每个Android手机上都可用。
Android代码中有各种视图类,这些类的实例变量取名时最好以View结尾(或其他能清晰表明视图属性的单词,如Button)。这样能避免误把视图对象当作model对象。下面的例子说明了随意起名可能导致的错误:
public class MainActivity {
private TextView mPrize;
private TextView mPrizeView; // 推荐:变量名以View结尾,表明自己是视图
...
private String getPrize() {
return mPrize + "元"; // 出错了!把视图误当作了model
}
...
}
在项目规模上去了之后,靠命名规范去尽可能地避免这样的低级错误,不失为一个办法。
comments powered by Disqus