天天看点

自定义lint规则解决开发中的问题

一、背景

在发贝壳2.1.1版本第一次灰度时遇到了一个崩溃(崩溃在租房),第二次灰度版本时发生了相同原因的崩溃(崩在了二手)。 当时的做法是发现一处解决一处, 但没发现的隐藏问题是个定时炸弹。

日志:

java.lang.RuntimeException: Parcelable encountered IOException writing serializable object (name = com.homelink.customer.host.manage.model.response.HostCommentBean) at android.os.Parcel.writeSerializable(Parcel.java:1823) at android.os.Parcel.writeValue(Parcel.java:1771) at android.os.Parcel.writeArrayMapInternal(Parcel.java:838)

二、问题原因

这个crash的原因是当前类实现了Serializable接口,但成员数据类型未实现Serializable接口, 导致Activity/Fragment在用Intent传值时出现序列化错误,最终崩在了1823行。

try {
1817            ObjectOutputStream oos = new ObjectOutputStream(baos);
1818            oos.writeObject(s);
1819            oos.close();
1820
1821            writeByteArray(baos.toByteArray());
1822        } catch (IOException ioe) {
1823            throw new RuntimeException("Parcelable encountered " +
1824                "IOException writing serializable object (name = " + name +
1825                ")", ioe);
1826        }
           

###三、解决思路

现有项目代码存在序列化崩溃的潜在风险,如何使用技术手段找出来呢? 以后如何不再犯相同错误? 现有的Lint、FindBugs、CheckStyle是备选方案, 最终因为Lint可以实时检查并智能提醒而选择使用Lint实现,即用工具实时提醒开发人员潜在风险。

自定义lint规则解决开发中的问题

期望效果

自定义lint规则解决开发中的问题

扫描结果 使用lint自定义规则扫描二手插件找到所有Serializable崩溃的风险点,从而从根本上解决Serializable序列化崩溃问题。

四、自定义Lint规则

感觉跟写Java业务代码很像, 只需要了解API功能就可以快速上手。 自定义Lint有个总入口IssueRegistry类,在List里返回需要检查的自定义规则即可。

public class SerializableDetector extends Detector implements Detector.UastScanner {

  private static final String CLASS_SERIALIZABLE = "java.io.Serializable";

  private String[] basicTypes = {"byte", "short", "int", "long", "float", "double",
      "char", "boolean", "byte[]", "short[]", "int[]", "long[]", "float[]", "double[]",
      "char[]", "boolean[]","java.lang.String", "java.lang.Double",
      "java.lang.Boolean", "java.lang.Long", "java.lang.Short",
      "java.lang.Integer", "java.lang.Char", "java.lang.Boolean","java.lang.String[]",
      "java.lang.Double[]", "java.lang.Boolean[]", "java.lang.Long[]", "java.lang.Short[]",
      "java.lang.Integer[]", "java.lang.Char[]", "java.lang.Boolean[]"};

  private static HashSet<String> hashSet = new HashSet<>();

  public static final Issue ISSUE = Issue.create(
      "ClassSerializable",
      "Bean类成员需要实现Serializable接口",
      "Bean类成员需要实现Serializable接口",
      Category.SECURITY, 5, Severity.ERROR,
      new Implementation(SerializableDetector.class, Scope.JAVA_FILE_SCOPE));

  @Nullable
  @Override
  public List<String> applicableSuperClasses() {
    //父类是"java.io.Serializable"
    return Collections.singletonList(CLASS_SERIALIZABLE);
  }

  @Override
  public void visitClass(JavaContext context, UClass declaration) {
    if (declaration instanceof UAnonymousClass) {
      return;
    }
    sortClass(context, declaration);
  }

  //递归直到基本数据类型
  private void sortClass(JavaContext context, UClass declaration) {
    if (hashSet.contains(declaration.getPsi().getQualifiedName())) {
      //参考动态规划的备忘录方式,计算出结果的类不再计算第二遍
      //System.out.println(declaration.getPsi().getQualifiedName() + "已经被过滤");
      return;
    }

    UastParser parser = context.getClient().getUastParser(context.getProject());
    boolean isSerialized = false;
    for (PsiClassType psiClassType : declaration.getImplementsListTypes()) {
      if (CLASS_SERIALIZABLE.equals(psiClassType.getCanonicalText())) {
        //实现了序列化
        isSerialized = true;
        break;
      }
    }
    //System.out.println("++++++" + declaration.getPsi().getQualifiedName());
    for (String type : basicTypes) {
      if (type.equalsIgnoreCase(declaration.getPsi().getQualifiedName())) {
        //基本数据类似不需要实现Serializable,继续判断其它成员变量
        return;
      }
    }

    if (!isSerialized) {
      if (!hashSet.contains(declaration.getPsi().getQualifiedName())) {
        context.report(ISSUE,
            declaration.getNameIdentifier(),
            context.getLocation(declaration.getNameIdentifier()),
            String.format("成员变量 `%1$s` 需要实现Serializable接口",
                declaration.getPsi().getQualifiedName()));
        hashSet.add(declaration.getPsi().getQualifiedName());
        System.out.println("size" + hashSet.size() + "/" + declaration.getPsi().getQualifiedName() + "没实现序列化");
      }
      return;
    }

    //检查内部类
    for (UClass uClass : declaration.getInnerClasses()) {
      //递归判断内部类, 查看成员参数是否实现了序列化方法
      sortClass(context, uClass);
    }

    //检查成员变量
    for (UField uField : declaration.getFields()) {
      boolean isBasic = false;
      for (String type : basicTypes) {
        if (type.equalsIgnoreCase(uField.getType().getCanonicalText())) {
          //基本数据类似不需要实现Serializable,继续判断其它成员变量
          isBasic = true;
          break;
        }
      }

      if (isBasic) {
        //如果是基本数据类型继续循环
        continue;
      }

      if (uField.getType().getCanonicalText()
          .matches("^[A-Za-z0-9.]*[List|Set]<[A-Za-z0-9.]*>$")) {
        //使用了泛型则判断泛型是否实现了Serializable
        String genericType = uField.getType().getCanonicalText().substring(
            uField.getType().getCanonicalText().indexOf("<") + 1,
            uField.getType().getCanonicalText().indexOf(">"));

        PsiClass cls = parser.getEvaluator().findClass(genericType);
        UClass uGeneric = context.getUastContext().getClass(cls);
        sortClass(context, uGeneric);
      } else {
        PsiClass psiClass = parser.getEvaluator()
            .findClass(uField.getType().getCanonicalText());
        sortClass(context, context.getUastContext().getClass(psiClass));
      }
    }
  }
}
           

五、集成方式

https://github.com/brycegao/lintrules

按照lint自定义规则的2种方式(jar或aar)集成即可。

用技术手段解决线上共性问题是写自定义lint规则的理由, 目的是相同错误不要犯第二次,逐渐降低崩溃率。

六、调试技巧

1、在Android项目中,以源码形式依赖自定义Lint代码。

2、在自定义Lint代码中打好断点。

3、在Android Application模块的build.gradle中关闭Lint的abortOnError选项。

lintOptions {

abortOnError false

}

4、在Android Studio的运行参数(Run Configurations)中添加一个Remote类型,命名为LintDebug,其它都取默认值即可。

自定义lint规则解决开发中的问题

5、在Android Studio的terminal里执行命令export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"设置临时环境变量,从而开启Gradle调试。端口号为默认的5005,和前面在Android Studio中新增的Run Configuration端口号一致

6、在terminal执行Gradle任务./gradlew clean lintDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true --no-daemon。执行后Gradle会等待Android Studio调试器连接。

7、在AS里选中刚才新建的Remote运行方式LintDebug,点击调试按钮就可以了。

自定义lint规则解决开发中的问题

7、在terminal执行unset GRADLE_OPTS关闭调试.

参考:

https://tech.meituan.com/android_custom_lint.html

https://www.jianshu.com/p/4833a79e9396 Android Lint增量扫描实战纪要