介绍我在项目中碰到的一些细小的改进的点,谈不上最佳实践,叫“更好的实践”吧

如何在发布一个Android应用时去掉日志

开发时打印日志,发布的应用不打印日志,这是几乎每个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文件。

Android 应用如何快速打渠道包

已知最快的办法是美团技术博客的这篇文章介绍的第三种方法。需要注意的是在生成渠道包后需要重新用 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
  }
}

阅读并重视API文档

有这么一行代码:

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手机上都可用。

变量名要能区分视图和model

Android代码中有各种视图类,这些类的实例变量取名时最好以View结尾(或其他能清晰表明视图属性的单词,如Button)。这样能避免误把视图对象当作model对象。下面的例子说明了随意起名可能导致的错误:

public class MainActivity {
  private TextView mPrize;
  private TextView mPrizeView; // 推荐:变量名以View结尾,表明自己是视图
  ...

  private String getPrize() {
    return mPrize + "元"; // 出错了!把视图误当作了model
  }
  ...
}

在项目规模上去了之后,靠命名规范去尽可能地避免这样的低级错误,不失为一个办法。

comments powered by Disqus