一.序

最近这段时间升级了一系列开发工具的版本,Android Studio也升级到了3.4 (好像3.5稳定版都出来了,等有空再尝试一下香不香)。升级后出现了某些界面运行时crash,并且crash报出来的信息有点诡异。经过了一整天的排查和调试,发现是由于升级了一系列工具后默认使用了R8引出来的问题。

什么是R8

R8 是 ProGuard 的替代工具,用于代码的压缩(shrinking)和混淆(obfuscation)。R8 和当前的代码缩减解决方案 Proguard 相比,R8 可以更快地缩减代码,同时改善输出大小。

更详细的R8内容阅读Android压缩混淆官方文档

二.Crash的出现和问题定位

一系列开发工具升级完成后开始了愉快的开发,开发调试什么的一切正常。直到Release包的时候,出现了Crash。由于Release包是无法断点调试的,按照国际惯例,只能在Bug统计平台上面查看崩溃信息。

java.lang.NullPointerException
throw with null exception...
4 Caused by:
5 java.lang.NullPointerException:throw with null exception
6 com.loopj.android.http.AsyncHttpClient.a(AsyncHttpClient.java:8)
7 com.loopj.android.http.AsyncHttpClient.<init>(AsyncHttpClient.java:4)
8 com.loopj.android.http.AsyncHttpClient.<init>(AsyncHttpClient.java:1)
10 xxx.base.BaseActivity.a(BaseActivity.java:1)
11 xxx.activity.NoticeDetailActivity.M(NoticeDetailActivity.java:6)
12 xxx.base.BaseViewActivity.B(BaseViewActivity.java:4)
13 xxx.activity.NoticeDetailActivity.B(NoticeDetailActivity.java:12)
14 xxx.base.BaseActivity.onCreate(BaseActivity.java:14)
15 xxx.activity.NoticeDetailActivity.onCreate(NoticeDetailActivity.java:1)

初一看,NullPointException太简单了,再想一下,好像不太对劲,开发的时候在同一个位置并没有出现这个异常,而且是在Activity onCreate()就崩的情况下出现。以过往的经验来看,在Release版本出现问题很大概率跟混淆有关。仔细看一下log,上面的崩溃信息只能看到AsyncHttpClient在初始化的时候崩溃了,由于已经混淆看不出更详细的信息,bug统计平台缺少mapping符号表文件的配置。上传一下…

java.lang.NullPointerException: throw with null exception
2 android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2856)
3 ......
4 java.lang.NullPointerException:throw with null exception
5 com.loopj.android.http.AsyncHttpClient.org.apache.http.conn.scheme.SchemeRegistry getDefaultSchemeRegistry(boolean,int,int)(AsyncHttpClient.java:8)
6 com.loopj.android.http.AsyncHttpClient.void <init>(boolean,int,int)(AsyncHttpClient.java:4)
7 com.loopj.android.http.AsyncHttpClient.void <init>()(AsyncHttpClient.java:1)
9 xxx.base.BaseActivity.void init(android.os.Bundle)(BaseActivity.java:1)
...
10 ##_parent_##1##_parent_##
11 ##_child_## com.loopj.android.http.RequestHandle requestGet(java.lang.String,int,java.lang.reflect.Type)##_child_##
12 xxx.activity.NoticeDetailActivity.void requestData()(NoticeDetailActivity.java:6)
13 xxx.base.BaseViewActivity.void initView()(BaseViewActivity.java:4)
14 xxx.activity.NoticeDetailActivity.void initView()(NoticeDetailActivity.java:12)
15 xxx.base.BaseActivity.void onCreate(android.os.Bundle)(BaseActivity.java:14)
16 xxx.activity.NoticeDetailActivity.void onCreate(android.os.Bundle)(NoticeDetailActivity.java:1)

信息稍微多了一些,可以看到崩溃的位置是在AsyncHttpClient类初始化时调用了getDefaultSchemeRegistry()方法时出现的崩溃,知道了崩溃位置,直接查看代码。

private static SchemeRegistry getDefaultSchemeRegistry(boolean fixNoHttpResponseException, int httpPort, int httpsPort) {if (fixNoHttpResponseException) {log.d(LOG_TAG, "Beware! Using the fix is insecure, as it doesn't verify SSL certificates.");}if (httpPort < 1) {httpPort = 80;log.d(LOG_TAG, "Invalid HTTP port number specified, defaulting to 80");}if (httpsPort < 1) {httpsPort = 443;log.d(LOG_TAG, "Invalid HTTPS port number specified, defaulting to 443");}// Fix to SSL flaw in API < ICS// See https://code.google.com/p/android/issues/detail?id=13117SSLSocketFactory sslSocketFactory;if (fixNoHttpResponseException) {sslSocketFactory = MySSLSocketFactory.getFixedSocketFactory();} else {sslSocketFactory = SSLSocketFactory.getSocketFactory();}SchemeRegistry schemeRegistry = new SchemeRegistry();schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), httpPort));schemeRegistry.register(new Scheme("https", sslSocketFactory, httpsPort));return schemeRegistry;
}

这一段是android-async-http库的源码,作为第三方的我来说,并没有对它进行任何改动,一般来说应该不会出现NullPointException。还是看不出问题具体出现在哪一句代码,看来只能用断点大法调试一下看看了。

断点定位走到SchemeRegistry schemeRegistry = new SchemeRegistry();这一句,直接就Crash掉了,只是一句普通的new对象操作,继续进入源码位置查看。

/** @deprecated */
@Deprecated
public final class SchemeRegistry {public SchemeRegistry() {throw new RuntimeException("Stub!");}public synchronized Scheme getScheme(String name) {throw new RuntimeException("Stub!");}public synchronized Scheme getScheme(HttpHost host) {throw new RuntimeException("Stub!");}public synchronized Scheme get(String name) {throw new RuntimeException("Stub!");}public synchronized Scheme register(Scheme sch) {throw new RuntimeException("Stub!");}public synchronized Scheme unregister(String name) {throw new RuntimeException("Stub!");}public synchronized List<String> getSchemeNames() {throw new RuntimeException("Stub!");}public synchronized void setItems(Map<String, Scheme> map) {throw new RuntimeException("Stub!");}
}

SchemeRegistry是org.apache.http包下一个类,但打开看到的只是一个存根,并没有具体的实现逻辑,类标签上打上了deprecated表示已废弃。继续没有更多的信息,搜索引擎一轮查找,在Android官方文档中找到了Apache Http弃用的说明内容。

Apache HTTP 客户端弃用
在 Android 6.0 中,我们取消了对 Apache HTTP 客户端的支持。 从 Android 9 开始,默认情况下该内容库已从 bootclasspath 中移除且不可用于应用。

之前猜测问题是由混淆引发的,so继续查找Apache Http + Proguard混淆相关的资料,混淆规则之类的内容并没有搜到,只在Apache Http的资料中了解到如下信息:在Android Version 23以上使用Apache Http将无法引用到相关的类,解决方法是在App libs下拷贝添加org.apache.http.legacy.jar包。于是在App libs目录下找了一遍,确实找到了对应的jar包,jar包里面的类跟上面的SchemeRegistry存根类是一样的。

到了这里再次陷入胡同,没有线索也没有查到已经遇到过的解决方法,可能真的Apache Http已经太旧没有人用了,毕竟现在主流的网络请求框架都是OkHttp。

反编译走起

本以为很简单可以解决的问题,没想到要走到反编译这一步,把自己的App反编译直接查看应该可以找到更多的线索。反编译这一招平时用的比较少,以前反编译还是比较麻烦,要几个工具配合起来用,甚至反编译出来看到的java代码有些地方都被截断逻辑不清晰,需要配合smali食用。现在有jadx这种强大的神器,反编译已经很方便了。

直接反了,找到Crash产生的地方,AsyncHttpClient.getDefaultSchemeRegistry()方法,虽然被混淆了方法名,但是跟着逻辑看,还是能看出来这个a()方法就是getDefaultSchemeRegistry()方法。

private static SchemeRegistry a(boolean z, int i, int i2) {String str = a;if (z) {m.d(str, "Beware! Using the fix is insecure, as it doesn't verify SSL certificates.");}if (i < 1) {m.d(str, "Invalid HTTP port number specified, defaulting to 80");}if (i2 < 1) {m.d(str, "Invalid HTTPS port number specified, defaulting to 443");}if (z) {MySSLSocketFactory.b();} else {SSLSocketFactory.getSocketFactory();}SchemeRegistry schemeRegistry = new SchemeRegistry();throw null;
}

throw null ??? throw null是什么神仙操作???

一脸懵逼的我把旧版本混淆的apk反出来查了一下相同的位置,这个位置的代码是正常的。看来一定是开发工具升级后导致的,再次一轮查资料,在多次尝试退版本和修改配置之后发现,当我在gradle properties中把R8关掉后(android.enableR8=false),一切正常了,反编译出来的代码也没有了throw null。

所以现在已经有了一种解决方案,直接把R8关掉,继续Proguard,一切正常。但,人生的意义在于折腾,我就是想要把R8开起来(斜眼)

三.折腾和测试

现在问题定位到R8开启后会出现了很多throw null把原来要执行的代码替换掉了,为什么会这样?

在反编译包中,通过全局搜索throw null这个关键字,搜到了613个结果。慢慢看一下throw null所在的代码有什么规律。

  1. 发现的第一个线索点,它是一段kotlin的代码,这里分别放出原始kotlin代码、开启R8后的反编译java代码、未开启R8的反编译java代码 三种版本进行对比

    原始kotlin代码

    override fun goToMain() {EventBus.getDefault().post(HomeDataMessageEvent(0))UIRouter.goToHome()if (activity != null) activity!!.finish()
    }
    

    开启R8后的反编译java代码

    public void q() {EventBus.c().c(new HomeDataMessageEvent(0));UIRouter.goToHome();if (getActivity() != null) {FragmentActivity activity = getActivity();if (activity != null) {activity.finish();} else {Intrinsics.e();throw null;}}
    }
    

    未开启R8的反编译java代码

    public void b() {EventBus.a().d(new HomeDataMessageEvent(0));UIRouter.goToHome();if (getActivity() != null) {FragmentActivity activity = getActivity();if (activity == null) {Intrinsics.a();}activity.finish();}
    }
    

    在上面的两份反编译代码中都出现了Intrinsics.x()方法,这个代码在原始代码中就是用于处理 activity!! 的,意思是断定activity不为空,如果为空的话就抛出异常,Intrinstics类抛出异常的逻辑如下

    public static void e() {Throwable kotlinNullPointerException = new KotlinNullPointerException();a(kotlinNullPointerException);throw ((KotlinNullPointerException) kotlinNullPointerException);
    }
    

    通过上面的分析可以发现开启R8和不开启R8其中一个不同点就是开启R8后,会在调用了抛出异常的方法位置后面插入一个throw null。

  2. 为什么会出现这个throw null,我们继续寻找其他throw null的代码进行观察,根据最初得到的Crash日志,我们回来继续观察最初的崩溃点,初始化SchemeRegistry之后被插入了一个throw null,根据上面的分析,开启R8会在抛出异常的代码后面插入一个throw null,这里初始化SchemeRegistry确实是抛出了一个异常,但也并没有抛出异常,为什么这么说,因为抛出异常的逻辑是Apache Http的jar包存根,在APP运行期间实际调用的逻辑是在Android SDK里面的,并不会调用到jar包抛异常的代码。

    分析到这里,其实这个Crash已经大概知道原因了,但是这个throw null到底是什么,还没有结论。继续沿着Crash路径往上查看代码,下面放出开启R8和未开启R8的两份反编译代码进行对比。

    //开启了R8
    private static SchemeRegistry a(boolean z, int i, int i2) {String str = a;if (z) {m.d(str, "Beware! Using the fix is insecure, as it doesn't verify SSL certificates.");}if (i < 1) {m.d(str, "Invalid HTTP port number specified, defaulting to 80");}if (i2 < 1) {m.d(str, "Invalid HTTPS port number specified, defaulting to 443");}if (z) {MySSLSocketFactory.b();} else {SSLSocketFactory.getSocketFactory();}SchemeRegistry schemeRegistry = new SchemeRegistry();throw null;
    }
    
    //未开启R8
    private static SchemeRegistry a(boolean z, int i, int i2) {SocketFactory c;String str = a;if (z) {m.b(str, "Beware! Using the fix is insecure, as it doesn't verify SSL certificates.");}if (i < 1) {i = 80;m.b(str, "Invalid HTTP port number specified, defaulting to 80");}if (i2 < 1) {i2 = 443;m.b(str, "Invalid HTTPS port number specified, defaulting to 443");}if (z) {c = MySSLSocketFactory.c();} else {c = SSLSocketFactory.getSocketFactory();}SchemeRegistry schemeRegistry = new SchemeRegistry();schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), i));schemeRegistry.register(new Scheme("https", c, i2));return schemeRegistry;
    }
    

    上面两份代码可以观察到,开启R8后出现了throw null,并且后面部分逻辑消失了,再沿着Crash路径往上查看。

    public HttpNewUtils(Context context, RequestParams requestParams, String str, int i, Handler handler, Type type, int i2) {this.e = requestParams;this.f = str;this.h = MainApplication.context();this.i = i;this.j = handler;this.k = type;this.g = new AsyncHttpClient();if (i2 != 0) {this.g.c(i2);}this.a = new PreferencesDataUtil(MainApplication.context());if (i2 != 0) {this.g.c(i2);}
    }
    public HttpNewUtils(Context context, RequestParams requestParams, String str, int i, Handler handler, Type type) {this.c = requestParams;this.d = str;this.f = MainApplication.context();this.g = i;this.h = handler;this.i = type;AsyncHttpClient asyncHttpClient = new AsyncHttpClient();throw null;
    }
    

    同样是插入了一句throw null,被截断了一部分代码,聪明如你,应该猜到了点什么。R8作为Proguard的替代品,它的作用是代码压缩和混淆,根据以上观察到的现象,基本上可以猜测R8在处理抛出异常时会把后续不再执行的代码进行删减,删减过后会插入一个throw null作为标记,这就是R8做代码压缩时的一个新特性。

    最后,我们来验证一下这个特性,只需要写一个必然会抛出异常的逻辑判断,观察打包后后续的代码是否被删减和插入throw null标记,即可验证我们的猜想。

    public boolean test(View v) throws Exception {if(true) throw new Exception("R8 Test");Log.d("R8 Test", "test: 1");Log.d("R8 Test", "test: 2");Log.d("R8 Test", "test: 3");return true;
    }//调用
    public void setListener() {try {test(timeTv);Log.d("R8 Test", "test: 4");Log.d("R8 Test", "test: 5");Log.d("R8 Test", "test: 6");} catch (Exception e) {e.printStackTrace();}
    }
    
    private boolean a(View view) throws Exception {throw new Exception("R8 Test");
    }//调用
    public void y() {try {a(this.timeTv);throw null;} catch (Exception e) {e.printStackTrace();}
    }
    

    跟猜想一致,对于抛出异常的代码在调用后会插入一句throw null,并且删减掉后续代码。

四.总结

经过上面定位和验证的过程,这个问题已经确定了。再重复一遍上面的结论。

  1. R8作为Proguard的替代品,它的作用是代码压缩和混淆,R8在处理抛出异常的时会把后续不再执行的代码进行删减,删减过后会插入一个throw null作为标记,这就是R8做代码压缩时的一个新特性。
  2. 产生上面这种问题并不是由R8单方面造成的,是由于Android已经废弃了Apache Http的使用,导致开发时无法引用到相关类,必须引入一个jar包存根来通过编译。虽然在实际调用的时候是调用Android SDK中的Apache Http代码,但编译过程中jar包存根被R8当作抛出异常来处理,把后续的代码压缩优化掉了。
  3. 影响范围:仍然在使用Apache Http的应用,在升级AS和Gradle默认开启R8后会遇到。ROM开发时部分系统可能会做一些内置API给系统应用使用,这种情况下如果单独做一套存根jar包导入到应用中,打包的时候使用了R8也会遇到这种问题。
  4. 解决方法,暂时来说有两种方法,一种是直接关闭R8,另一种是不使用方法存根类,在上述问题中也可以把已废弃的Apache Http替换掉。
  5. 暂时没有找到可以用Proguard规则规避掉这个问题的办法,如果有了解的欢迎留言。

踩到一个R8代码压缩工具的坑相关推荐

  1. 踩坑了、踩到一个特别无语的常识坑

    大家好 踩坑了啊,又踩坑了啊! 这次踩到一个特别无语的常识坑.知道真相的那一刻,人就是整个麻掉. 先上个代码: private static double calculate(double a, in ...

  2. 踩过一个FM24C64与FM24CL64的坑

    最近干活踩了一个坑,由于要将系统数据存在外部,硬件选型选了一款8K的铁电存储器,驱动方式是IIC.本来这个活非常简单,芯片的时序也不是很复杂,所以最开始觉得这个活不是很复杂,结果调试搞了一上午,心态有 ...

  3. android字节流压缩,Android代码压缩工具R8详解 android.enableR8=true

    前言 最近 Android Studio 稳定版迎来了3.3版本更新,带来的新特性之一是新一代的代码压缩工具 R8,本文将详细介绍这一新工具 R8.阅读本文内容前需了解: 关于 R8 作为 Andro ...

  4. i/o timeout , 希望你不要踩到这个net/http包的坑

    问题 我们来看一段日常代码. 1package main23import (4 "bytes"5 "encoding/json"6 "fmt" ...

  5. [JavaScript] 多数前端工程师都没注意到的一个关于console.log()的坑

    [JavaScript] 多数前端工程师都没注意到的一个关于console.log()的坑 请阅读以下代码并猜测结果: function test() {let obj = {}, arr=[]for ...

  6. JavaScript代码压缩工具UglifyJS和Google Closure Compiler的基本用法

    网上搜索了,目前主流的Js代码压缩工具主要有Uglify.YUI Compressor.Google Closure Compiler,简单试用了UglifyJS 和Google Closure Co ...

  7. css游戏代码_介绍CSSBattle-第一个CSS代码搜寻游戏

    css游戏代码 by kushagra gour 由kushagra gour 介绍CSSBattle-第一个CSS代码搜寻游戏 (Introducing CSSBattle - the first ...

  8. AI一分钟|阿里云解释故障原因:触发了一个未知代码Bug;清华蝉联ISC18超算竞赛总冠军...

     ▌阿里云发说明解释昨日故障原因:触发了一个未知代码Bug 今日凌晨,阿里云官方微博针对昨日出现的大规模故障问题作出官方回应.声明称,在运维上的一个操作失误,导致一些客户访问阿里云官网控制台和使用 ...

  9. iOS 11开发教程(七)编写第一个iOS11代码Hello,World

    iOS 11开发教程(七)编写第一个iOS11代码Hello,World 代码就是用来实现某一特定的功能,而用计算机语言编写的命令序列的集合.现在就来通过代码在文本框中实现显示"Hello, ...

最新文章

  1. 各大知名企业的Research展示
  2. Spring的环绕通知
  3. 深度学习100例 - 卷积神经网络(Inception V3)识别手语 | 第13天
  4. 数据库性能检查指导方案
  5. bpsk信道编码matlab,信道编码-研究日记_3 10/14/2016
  6. PHP 学习 第一天
  7. SQL性能优化工具TKPROF
  8. Mysql中的转义字符
  9. MySql noinstall-5.1.34-win32 配置
  10. 【函数计算月报】2018年12月刊
  11. 如何才能写出“高质量”的代码?
  12. 微软应用商店_微软商店那些好用的UWP软件!你不看这篇文章会后悔的!超级实用! | APP杂货店...
  13. ABBYY最新版本OCR文字图像识别软件v16
  14. cocos2d js 别出白线游戏上线
  15. 怎样裁剪PDF文件中的页面
  16. Android字体加粗,UI小姐姐说太粗了,解决办法
  17. 智能在线客服是如何工作的?
  18. C++ string assign和append的常用方法
  19. Java基础-OOP 面向对象编程
  20. 蓝牙 韦东山_韦东山生活实例演绎法讲解蓝牙

热门文章

  1. python---FlaskAPI基本用法
  2. js 实现俄罗斯方块
  3. 2022年最新丨中国建筑、中国石化等40家头部央企「数字化转型」路线图发布
  4. 同济大学21年计算机考研情况 招生人数较少,难度较大
  5. 如何迎战思科?国外对手扛不住华为的杀价游戏
  6. 震旦adc218网络ip设置方法
  7. HTML5网页在线代码编辑器源码 适用各类项目代码在线编辑
  8. Pytorch获取中间变量的梯度grad
  9. 一个不错的英语聊天室
  10. 生态环境数据下载网站