一、背景
在发贝壳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自定义规则扫描二手插件找到所有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,其它都取默认值即可。
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,点击调试按钮就可以了。
7、在terminal执行unset GRADLE_OPTS关闭调试.
参考:
https://tech.meituan.com/android_custom_lint.html
https://www.jianshu.com/p/4833a79e9396 Android Lint增量扫描实战纪要