本文共 20883 字,大约阅读时间需要 69 分钟。
该博文用来整理开源项目 迁移到 AndroidX 过程中遇到的各种问题。
? Android-References 是一个 Android 示例程序项目:包含了 MVP, MVVM, 组件化, ARouter, RxJava, EventBus, ButterKnife, 视频播放, 视频直播, 网络访问, 布局和控件整理等,感性却可以到 Gihub: 参考源码。
因为该项目包含了各种 support 包控件,并且各种组件化、架构模式和各种常见的功能一应俱全,因此,比较具有代表性。这里我们使用它来作为一个示例来将我们的项目迁移到 AndroidX。
关于 AndroidX,可以参考:
AndroidX 用来统一 Android 中的 support 包,之前我们通过引入 support 包的各个版本来使用支持包,现在我们可以通过使用 AndroidX 来使用支持包。从长远来看这当然是大有好处的,可以避免使用支持包中遇到的版本冲突、升级带来的各种问题。
不过,如果项目完全迁移到 AndroidX 风险还是太大。如果项目紧急的话,引入 AndroidX 成本都有些高,主要是因为一些三方库的原因。虽然一些使用特别多的三方库,比如 Glide 等都已经开始支持 AndroidX。当然,还有一些潜在的问题,比如使用字符串来获取类的 Behavior 等,迁移的时候可能就不会被照顾到。
为了迁移到 AndroidX,Google 给开发者提供了一个工具:在 AS 的 Refactor 中提供了一项 Migrate to AndroidX 的选项。但选择了迁移之后,出现一些问题还需要开发者自己手动解决。
这里我们迁移之后在 build 的时候出现了标题的问题:
* What went wrong:Execution failed for task ':app:transformClassesWithMultidexlistForAlphaDebug'.> com.android.build.api.transform.TransformException: Error while generating the main dex list.
显然是将 class 转换成 dex 的过程中出现了一些问题,不过只是上面的这行日志我们无法定位问题。所以,我们需要让 gradle 输出更多的错误信息,于是我们执行:
gradlew build --stacktrace
来让 Gradle 输出错误栈信息:
Caused by: com.android.builder.multidex.D8MainDexList$MainDexListException: com.android.tools.r8.errors.CompilationError: Program type already present: com.alibaba.android.arouter.routes.ARouter$$Group$$library at com.android.builder.multidex.D8MainDexList.generate(D8MainDexList.java:87) at com.android.build.gradle.internal.transforms.D8MainDexListTransform.transform(D8MainDexListTransform.kt:131) ... 54 more
显然是 ARouter$$Group$$library
类的问题,我们使用 Ctrl+N
来搜索这个类,发现出现了两个同样的类。这个类是使用阿里的 ARouter 的时候在编译期间生成的:
按照上面的错误提示,该类同时出现在了我们的两个模块 libraries
和 layout
下面,因而类冲突了。所以,接下来的问题就是要发现为什么会出现类冲突。
经过一层层排查发现是一个地方写错了:
这个是 layout
模块下面的冲突的类,我们发现它的路由地址是 /library/swipe_back
,所以因为路由的地址是 library
的原因它在 layout
的模块下面生成了 ARouter$$Group$$Library
。按照正确的写法它应该是出现在 layout
模块下面,并且路由的地址是 /layout/swipe_back
,那样就应该被生成到 ARouter$$Group$$Layout
下面,就不会多出一个类 ARouter$$Group$$Library
了。
虽然,最终问题的原因很简单,但是我们看到,发现问题的过程中需要自己有思路的去排查,而不是除了问题立刻 Google 或者 SOF。
修改了上面的问题之后,我们的程序可以编译并且安装了。
然后,我们又遇到下面的问题:
android.view.InflateException: Binary XML file line #14: Error inflating class
这种问题比较常见,是 XML 的某个地方写错了,经过排查发现有一行代码,当我们为控件添加 Behavior 的时候使用了字符串形式的类名。在迁移的时候没有被照顾到:
事实上在 Google 的新的 material 包下面的 values.xml 文件中定义了一些 Behavior,新的包中这些字符串的值已经被替换过。但是像我们上面的这种情况,因为使用的是字符串而不是引用的资源,所以就没有被替换过去。因此,引用非自定义的 Behavior 的时候需要注意使用字符串资源进行引用而不是使用字符串:
这里的类 Support28Activity
用来整理原来的 support-28 包里面的一些控件,迁移之后页面打开的时候立即崩溃。然后留下了一地鸡毛(异常):
java.lang.RuntimeException: Unable to start activity ComponentInfo{me.shouheng.references/me.shouheng.layout.view.support28.Support28Activity}: android.view.InflateException: Binary XML file line #12: Binary XML file line #12: Error inflating class com.google.android.material.button.MaterialButton
我们尝试在程序中寻找 MaterialButton
,发现确实能够找到这个类,但这里加载失败是什么原因呢?
于是我们继续查看输出的错误日志,从这里我们获取到了更多的信息。
Process: me.shouheng.references, PID: 22961 java.lang.RuntimeException: Unable to start activity ComponentInfo{me.shouheng.references/me.shouheng.layout.view.support28.Support28Activity}: android.view.InflateException: Binary XML file line #12: Binary XML file line #12: Error inflating class com.google.android.material.button.MaterialButton at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3037) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3172) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1906) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6863) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) Caused by: android.view.InflateException: Binary XML file line #12: Binary XML file line #12: Error inflating class com.google.android.material.button.MaterialButton Caused by: android.view.InflateException: Binary XML file line #12: Error inflating class com.google.android.material.button.MaterialButton Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at android.view.LayoutInflater.createView(LayoutInflater.java:647) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:790) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730) at android.view.LayoutInflater.rInflate(LayoutInflater.java:863) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824) at android.view.LayoutInflater.inflate(LayoutInflater.java:515) at android.view.LayoutInflater.inflate(LayoutInflater.java:423) at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:126) at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:95) at me.shouheng.commons.view.activity.CommonActivity.onCreate(CommonActivity.java:41) at android.app.Activity.performCreate(Activity.java:7149) at android.app.Activity.performCreate(Activity.java:7140) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1288) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3017) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3172) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1906) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6863) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.MaterialComponents (or a descendant). at com.google.android.material.internal.ThemeEnforcement.checkTheme(ThemeEnforcement.java:240) at com.google.android.material.internal.ThemeEnforcement.checkMaterialTheme(ThemeEnforcement.java:215) at com.google.android.material.internal.ThemeEnforcement.checkCompatibleTheme(ThemeEnforcement.java:143)2018-11-10 15:21:55.948 22961-22961/me.shouheng.references E/AndroidRuntime: at com.google.android.material.internal.ThemeEnforcement.obtainStyledAttributes(ThemeEnforcement.java:78) at com.google.android.material.button.MaterialButton.(MaterialButton.java:140) at com.google.android.material.button.MaterialButton. (MaterialButton.java:133) ... 27 more
按照后面几行的意思:
Caused by: java.lang.IllegalArgumentException: The style on this component requires your app theme to be Theme.MaterialComponents (or a descendant).
这个错误是因为我们设置的主题照成的。按照上面的意思,我们需要更新自己的控件的主题到 Theme.MaterialComponents
。除了切换控件的主题,我们可以更新应用的主题,我们可以让自己的应用主题继承 Material Components Bridge
主题:
可以根据自己的需要选择继承下面的几种主题:
Theme.MaterialComponents.BridgeTheme.MaterialComponents.Light.BridgeTheme.MaterialComponents.NoActionBar.BridgeTheme.MaterialComponents.Light.NoActionBar.BridgeTheme.MaterialComponents.Light.DarkActionBar.Bridge
或者在你之前的 AppCompact
主题之上增加一些新的属性:
显然,这里是要求你为了使自己的应用符合 Material 规范,需要预先定义一些属性值,然后会被应用到程序的控件中。
这里我们将主题稍做修改
按照上面的方式,我们成功地打开了该页面。
然而打开了页面不久就又发现了问题,这个问题出现在 MaterialButton
的点击的时候:
Process: me.shouheng.references, PID: 5730 java.lang.IllegalArgumentException: Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed at android.graphics.Canvas.checkValidClipOp(Canvas.java:779) at android.graphics.Canvas.clipRect(Canvas.java:826) at com.google.android.material.shape.MaterialShapeDrawable.prepareCanvasForShadow(MaterialShapeDrawable.java:850) at com.google.android.material.shape.MaterialShapeDrawable.draw(MaterialShapeDrawable.java:746) at android.view.View.getDrawableRenderNode(View.java:20622) at android.view.View.drawBackground(View.java:20558) at android.view.View.draw(View.java:20357) at android.view.View.updateDisplayListIfDirty(View.java:19241) at android.view.View.draw(View.java:20094) at android.view.ViewGroup.drawChild(ViewGroup.java:4337) at androidx.coordinatorlayout.widget.CoordinatorLayout.drawChild(CoordinatorLayout.java:1246) at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4116) at android.view.View.updateDisplayListIfDirty(View.java:19232) at android.view.View.draw(View.java:20094) at android.view.ViewGroup.drawChild(ViewGroup.java:4337) at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4116) at android.view.View.updateDisplayListIfDirty(View.java:19232) at android.view.View.draw(View.java:20094) at android.view.ViewGroup.drawChild(ViewGroup.java:4337) at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4116) at android.view.View.updateDisplayListIfDirty(View.java:19232) at android.view.View.draw(View.java:20094) at android.view.ViewGroup.drawChild(ViewGroup.java:4337) at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4116) at android.view.View.updateDisplayListIfDirty(View.java:19232) at android.view.View.draw(View.java:20094) at android.view.ViewGroup.drawChild(ViewGroup.java:4337) at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4116) at android.view.View.updateDisplayListIfDirty(View.java:19232) at android.view.View.draw(View.java:20094) at android.view.ViewGroup.drawChild(ViewGroup.java:4337) at android.view.ViewGroup.dispatchDraw(ViewGroup.java:4116) at android.view.View.draw(View.java:20369) at com.android.internal.policy.DecorView.draw(DecorView.java:781) at android.view.View.updateDisplayListIfDirty(View.java:19241) at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:690) at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:696) at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:805) at android.view.ViewRootImpl.draw(ViewRootImpl.java:3515) at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:3312) at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2681) at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1633) at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7786) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1004) at android.view.Choreographer.doCallbacks(Choreographer.java:816) at android.view.Choreographer.doFrame(Choreographer.java:751) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:990) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6863) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
经过排查发现,在 com.google.android.material.shape.MaterialShapeDrawable
中的 prepareCanvasForShadow()
方法中使用了 Region.Op.REPLACE
,
private void prepareCanvasForShadow(Canvas canvas) { // ... 无关代码 canvas.clipRect(canvasClipBounds, Region.Op.REPLACE); // ... 无关代码 }
而在 support-28 中增加了下面的校验(这个校验在 support-27 上面是不存在的),显然是因为我们使用了 Region.Op.REPLACE
属性的原因:
private static void checkValidClipOp(@NonNull Region.Op op) { if (sCompatiblityVersion >= Build.VERSION_CODES.P && op != Region.Op.INTERSECT && op != Region.Op.DIFFERENCE) { throw new IllegalArgumentException( "Invalid Region.Op - only INTERSECT and DIFFERENCE are allowed"); } }
在该方法上面有真么几行注释:
Region.Op values other than Region.Op.INTERSECT and Region.Op.DIFFERENCE have the ability to expand the clip. The canvas clipping APIs are intended to only expand the clip as a result of a restore operation. This enables a view parent to clip a canvas to clearly define the maximal drawing area of its children. The recommended alternative calls are clipRect(Rect) and clipOutRect(Rect)
大体意思是:Region.Op.INTERSECT和Region.Op.DIFFERENCE以外的Region.Op值可以展开 Clip。画布剪辑API仅用于在还原操作后展开Clip。这使视图父级能够剪切画布以清楚地定义其子画面的最大绘制区域。推荐调用clipRect(Rect)和clipOutRect(Rect).
虽然我们找到了问题的原因,但是这个类究竟是在哪里用到的我们还无法定位到,因此,我们还需要进一步进行排查。
经过排查,我们发现出现问题的原因是 material 包中的 BottomAppBar
控件。其实从上面的栈中我们也可以看出这一点:明显是在绘制 View 树的子 View 的时候出现的异常,而且是绘制 CoordinatorLayout
。在 BottomAppBar
中使用了 materialShapeDrawable
,该变量是 MaterialShapeDrawable
。正是出现问题的罪魁祸首。
这个类MaterialShapeDrawable
是用来实现 BottomBar 的阴影效果的,虽然它是 BottomBar 的内部类,但是如果不希望它调用上面的那个方法也是可以的。
首先,我发现它要先判断是否具有阴影再调用绘制阴影的方法:
if (hasCompatShadow()) { // Save the canvas before changing the clip bounds. canvas.save(); prepareCanvasForShadow(canvas); // ...无关代码 }
而这里的判断是否具有阴影的方法如下:
private boolean hasCompatShadow() { return shadowCompatMode != SHADOW_COMPAT_MODE_NEVER && shadowCompatRadius > 0 && (shadowCompatMode == SHADOW_COMPAT_MODE_ALWAYS || requiresCompatShadow()); }
我们可以通过为 BottomBar 设置移除阴影效果来避免这个类调用绘制阴影的方法。于是,
虽然效果不好看,但是阴影解决了。
在排查上面问题的过程中,我们同样发现了 FAB 的一个问题。这个 Material 包中的 FAB 与 Support-28 包中的相比做了一些调整。且看下面的异常信息。这里问题显然是类无法记载造成的。那么具体是因为什么呢?我们往下看到错误栈的最后几行,发现是有一个属性没有找到。
2018-11-10 16:56:48.408 27581-27581/me.shouheng.references E/AndroidRuntime: FATAL EXCEPTION: main Process: me.shouheng.references, PID: 27581 java.lang.RuntimeException: Unable to start activity ComponentInfo{me.shouheng.references/me.shouheng.layout.view.support28.BottomAppBarActivity}: android.view.InflateException: Binary XML file line #24: Binary XML file line #24: Error inflating class com.google.android.material.floatingactionbutton.FloatingActionButton at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3037) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3172) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1906) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6863) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) Caused by: android.view.InflateException: Binary XML file line #24: Binary XML file line #24: Error inflating class com.google.android.material.floatingactionbutton.FloatingActionButton Caused by: android.view.InflateException: Binary XML file line #24: Error inflating class com.google.android.material.floatingactionbutton.FloatingActionButton Caused by: java.lang.reflect.InvocationTargetException at java.lang.reflect.Constructor.newInstance0(Native Method) at java.lang.reflect.Constructor.newInstance(Constructor.java:343) at android.view.LayoutInflater.createView(LayoutInflater.java:647) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:790) at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730) at android.view.LayoutInflater.rInflate(LayoutInflater.java:863) at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824) at android.view.LayoutInflater.inflate(LayoutInflater.java:515) at android.view.LayoutInflater.inflate(LayoutInflater.java:423) at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:126) at androidx.databinding.DataBindingUtil.inflate(DataBindingUtil.java:95) at me.shouheng.commons.view.activity.CommonActivity.onCreate(CommonActivity.java:41) at android.app.Activity.performCreate(Activity.java:7149) at android.app.Activity.performCreate(Activity.java:7140) at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1288) at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3017) at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3172) at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1906) at android.os.Handler.dispatchMessage(Handler.java:106) at android.os.Looper.loop(Looper.java:193) at android.app.ActivityThread.main(ActivityThread.java:6863) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858) Caused by: java.lang.UnsupportedOperationException: Failed to resolve attribute at index 0: TypedValue{t=0x2/d=0x7f0300b3 a=3} at android.content.res.TypedArray.getColorStateList(TypedArray.java:565) at com.google.android.material.resources.MaterialResources.getColorStateList(MaterialResources.java:65) at com.google.android.material.floatingactionbutton.FloatingActionButton.(FloatingActionButton.java:204)2018-11-10 16:56:48.408 27581-27581/me.shouheng.references E/AndroidRuntime: at com.google.android.material.floatingactionbutton.FloatingActionButton. (FloatingActionButton.java:190) ... 27 more
根据错误的栈信息,我们到指定的方法下面去查看问题的原因,发现是设置背景着色的时候出现的问题:
backgroundTint = MaterialResources.getColorStateList( context, a, R.styleable.FloatingActionButton_backgroundTint);
但我们并没有为 FAB 添加该属性,那么又是哪里出现的问题呢?原来是因为我们为 FAB 添加了一个 style:
style="@style/Widget.MaterialComponents.FloatingActionButton"
而该 style
的定义是:
这样刚好与我们的问题吻合,所以解决的方案就是移除这个属性。
按照上面的方式对我们的项目做了调整之后,我们发现项目已经可以运行了。接下来出现的问题是网络访问过程中出现的。当我们访问网络的时候会遇到下面的这个异常:
java.net.UnknownServiceException: CLEARTEXT communication to baobab.kaiyanapp.com not permitted by network security policy
这个是因为 Android P 中新引入了网络安全规则,以上内容会对使用 http
的 URL 出现,默认会禁止访问 http 类型的地址。
当然,通常我们发布的时候会使用 Https 类型的网络协议,而当开发和调试的时候可能就没有那么严格了。所以,为了解决这个问题, Android 新引入了下面的解决方案:
首先,创建配置文件 res/xml/network_security_config.xml
,内容如下:
localhost
这里的 ``localhost是 host 的地址,比如我上面的出错的地址应该是
kaiyanapp.com`。
然后,我们将其配置到 manifest.xml
中:
在我们的项目中使用了三个不同的地址,并且都是 http 的,所以就需要在该地址下面配置三个域名,然后再次访问网络,问题就解决了。
关于 Android 网络和安全的内容,建议参考官方文档:。
显然迁移到 AndroidX 的过程中还是会出现许多问题的,大多数是与 Android 的支持包有关的。按照我们上面遇到的问题,主要包括以下几点内容:
好了,这篇文章大致到这里。也希望通过这篇文章来让你了解下迁移到 AndroidX 过程中可能会出现什么问题,并以此来考虑迁移的成本。
如果您喜欢我的文章,可以在以下平台关注我:
转载地址:http://okurf.baihongyu.com/