一开启R8代码压缩就崩溃?这问题得解决啊!

640?wx_fmt=jpeg


/   今日科技快讯   /


近日,网传有网友恶意薅羊毛致农民淘宝店被迫倒闭一事引发关注。淘宝官方微博1就此事做出回应,称已经第一时间对店铺进行保护,会在法律、规则允许的情况下,尽最大可能减少各方损失。此外,淘宝官方微博还表示,将坚决抵制恶意下单的“羊毛党”。


/   作者简介   /


周五的早上大家好,辛苦的一周即将结束,让我们在一个愉快的周末中迎接双11吧。


本篇文章来自苦逼程序员的投稿,分享了他在使用R8时遇到的一个异常,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。


苦逼程序员的博客地址:

https://blog.csdn.net/xyq046463


/   序   /


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


什么是R8


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


更详细的R8内容阅读


Android压缩混淆官方文档

https://developer.android.google.cn/studio/build/shrink-code?hl=zh-cn


/   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=13117
    SSLSocketFactory 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。


640?wx_fmt=png


反编译走起


本以为很简单可以解决的问题,没想到要走到反编译这一步,把自己的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;
}


640?wx_fmt=jpeg


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 void b() {
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(truethrow 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规则规避掉这个问题的办法



推荐阅读:
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值