Android 开发中遇到的 bug(7)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/willway_wang/article/details/91604164

前言

记录开发中遇到的 bug,不再让自己重复地被同样的 bug 折磨。

正文

1. android.view.WindowLeaked: Activity com.leapsi.sudoku.puzzle.ui.game.GameActivity has leaked window DecorView@5ff69ec[GameActivity] that was originally added here

时间:2019年6月12日19:41:44
错误日志:

2019-06-12 19:39:28.726 12092-12092/com.leapsi.sudoku.puzzle E/WindowManager: android.view.WindowLeaked: Activity com.leapsi.sudoku.puzzle.ui.game.GameActivity has leaked window DecorView@5ff69ec[GameActivity] that was originally added here
        at android.view.ViewRootImpl.<init>(ViewRootImpl.java:620)
        at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:363)
        at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:129)
        at android.app.Dialog.show(Dialog.java:471)
        at com.leapsi.sudoku.puzzle.ui.game.GameActivity.showGameOverDialog(GameActivity.kt:394)
        at com.leapsi.sudoku.puzzle.ui.game.GamePresenter.error(GamePresenter.kt:224)
        at com.leapsi.sudoku.puzzle.ui.game.GameActivity$showPuzzleLoaded$1.invoke(GameActivity.kt:319)
        at com.leapsi.sudoku.puzzle.ui.game.GameActivity$showPuzzleLoaded$1.invoke(GameActivity.kt:38)
        at com.leapsi.sudoku.puzzle.views.PuzzleView.setSelectedTile(PuzzleView.kt:307)
        at com.leapsi.sudoku.puzzle.views.KeyPad$setListeners$1.onClick(KeyPad.kt:40)
        at android.view.View.performClick(View.java:6652)
        at android.view.View.performClickInternal(View.java:6624)
        at android.view.View.access$3100(View.java:787)
        at android.view.View$PerformClick.run(View.java:26213)
        at android.os.Handler.handleCallback(Handler.java:891)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:207)
        at android.app.ActivityThread.main(ActivityThread.java:7470)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:524)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:958)

问题分析:在 Activity 里弹出了 Dialog,先 finish 掉 Activity,再 dismiss Dialog 造成的。
解决办法:先 dismiss Dialog,再 finish Activity。

2. Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x3040005

时间:2019年6月17日20:03:56
问题描述:

06-17 20:04:13.410 652-652/com.amtb.myapplication E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.amtb.myapplication, PID: 652
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.amtb.myapplication/com.amtb.myapplication.WebViewActivity}: android.view.InflateException: Binary XML file line #11: Error inflating class <unknown>
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2428)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2490)
        at android.app.ActivityThread.access$1200(ActivityThread.java:159)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1363)
        at android.os.Handler.dispatchMessage(Handler.java:102)
        at android.os.Looper.loop(Looper.java:135)
        at android.app.ActivityThread.main(ActivityThread.java:5569)
        at java.lang.reflect.Method.invoke(Native Method)
        at java.lang.reflect.Method.invoke(Method.java:372)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:931)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726)
     Caused by: android.view.InflateException: Binary XML file line #11: Error inflating class <unknown>
        at android.view.LayoutInflater.createView(LayoutInflater.java:652)
        at com.android.internal.policy.impl.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:55)
        at com.android.internal.policy.impl.HwPhoneLayoutInflater.onCreateView(HwPhoneLayoutInflater.java:75)
        at android.view.LayoutInflater.onCreateView(LayoutInflater.java:701)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:760)
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:825)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:523)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:425)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:368)
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:543)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:136)
        at com.amtb.myapplication.WebViewActivity.onCreate(WebViewActivity.java:27)
        at android.app.Activity.performCreate(Activity.java:6078)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1109)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2381)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2490) 
        at android.app.ActivityThread.access$1200(ActivityThread.java:159) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1363) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:135) 
        at android.app.ActivityThread.main(ActivityThread.java:5569) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at java.lang.reflect.Method.invoke(Method.java:372) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:931) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726) 
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Constructor.newInstance(Native Method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:288)
        at android.view.LayoutInflater.createView(LayoutInflater.java:626)
        at com.android.internal.policy.impl.PhoneLayoutInflater.onCreateView(PhoneLayoutInflater.java:55) 
        at com.android.internal.policy.impl.HwPhoneLayoutInflater.onCreateView(HwPhoneLayoutInflater.java:75) 
        at android.view.LayoutInflater.onCreateView(LayoutInflater.java:701) 
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:760) 
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:825) 
        at android.view.LayoutInflater.inflate(LayoutInflater.java:523) 
        at android.view.LayoutInflater.inflate(LayoutInflater.java:425) 
        at android.view.LayoutInflater.inflate(LayoutInflater.java:368) 
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:543) 
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:136) 
        at com.amtb.myapplication.WebViewActivity.onCreate(WebViewActivity.java:27) 
        at android.app.Activity.performCreate(Activity.java:6078) 
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1109) 
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2381) 
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2490) 
        at android.app.ActivityThread.access$1200(ActivityThread.java:159) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1363) 
        at android.os.Handler.dispatchMessage(Handler.java:102) 
        at android.os.Looper.loop(Looper.java:135) 
        at android.app.ActivityThread.main(ActivityThread.java:5569) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at java.lang.reflect.Method.invoke(Method.java:372) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:931) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726) 
     Caused by: android.content.res.Resources$NotFoundException: String resource ID #0x3040005
        at android.content.res.HwResources.getText(HwResources.java:1299)
        at android.content.res.Resources.getString(Resources.java:398)
        at com.android.org.chromium.content.browser.ContentViewCore.setContainerView(ContentViewCore.java:733)
        at com.android.org.chromium.content.browser.ContentViewCore.initialize(ContentViewCore.java:657)
        at com.android.org.chromium.android_webview.AwContents.createAndInitializeContentViewCore(AwContents.java:649)
        at com.android.org.chromium.android_webview.AwContents.setNewAwContents(AwContents.java:798)
        at com.android.org.chromium.android_webview.AwContents.<init>(AwContents.java:634)
        at com.android.org.chromium.android_webview.AwContents.<init>(AwContents.java:571)
        at com.android.webview.chromium.WebViewChromium.initForReal(WebViewChromium.java:332)
        at com.android.webview.chromium.WebViewChromium.access$100(WebViewChromium.java:102)
        at com.android.webview.chromium.WebViewChromium$1.run(WebViewChromium.java:284)
        at com.android.webview.chromium.WebViewChromium$WebViewChromiumRunQueue.drainQueue(WebViewChromium.java:129)
        at com.android.webview.chromium.WebViewChromium$WebViewChromiumRunQueue$1.run(WebViewChromium.java:116)
        at com.android.org.chromium.base.ThreadUtils.runOnUiThread(ThreadUtils.java:144)
        at com.android.webview.chromium.WebViewChromium$WebViewChromiumRunQueue.addTask(WebViewChromium.java:113)
    	at com.android.webview.chromium.WebViewChromium.init(WebViewChromium.jav

机型:HUAWEI HUAWEI P7-L07(Android5.1.1,API22),OPPO 5.0 API21
解决办法:
这次我用到了 MaterialComponents 的主题,首先就怀疑它们了。
测试了一下,
xml 代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <WebView
            android:id="@+id/webView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
</LinearLayout>

使用 AppCompatActivity(androidx 包下) + webview + AppCompat (Theme.AppCompat.Light.DarkActionBar)主题,不报错;

package com.amtb.myapplication;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebView;

import androidx.appcompat.app.AppCompatActivity;

public class WebViewActivity extends AppCompatActivity {

    private WebView webView;
    public static void start(Context context) {
        Intent starter = new Intent(context, WebViewActivity.class);
        context.startActivity(starter);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.webview_activity);
        webView = findViewById(R.id.webView);
        webView.loadUrl("https://www.baidu.com");
    }
}

使用 AppCompatActivity(androidx 包下) + webview + MaterialComponents (Theme.MaterialComponents.Light.DarkActionBar)主题,报错

报错指向了这个主题,查看一下依赖的包:

implementation 'com.google.android.material:material:1.1.0-alpha06'

怀疑这个 alpha 版本是不稳定的,换成稳定的依赖:

implementation 'com.google.android.material:material:1.0.0'

重新测试,不报错了。
另外使用 Activity + webview,不报错。

package com.amtb.myapplication;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.webkit.WebView;

public class WebViewActivity2 extends Activity {

    private WebView mWebView;

    public static void start(Context context) {
        Intent starter = new Intent(context, WebViewActivity2.class);
        context.startActivity(starter);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.webview_activity);
        mWebView =  findViewById(R.id.webView);
        mWebView.loadUrl("https://www.baidu.com");
    }
}

从以上验证可以得出,使用了 com.google.android.material 包不稳定的的 alpha 版本导致 在 5.x 手机上会报错。
所以,应当去使用稳定的依赖。

3. Caused by: java.lang.IllegalStateException: Auth.GoogleSignInApi.get…nInResultFromIntent(data) must not be null

时间:2019年6月18日20:10:45
问题描述:

2019-06-18 12:13:38.868 2728-2728/com.leapsi.sudoku.puzzle E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.leapsi.sudoku.puzzle, PID: 2728
    java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=9001, result=0, data=null} to activity {com.leapsi.sudoku.puzzle/com.leapsi.sudoku.puzzle.ui.MainActivity}: java.lang.IllegalStateException: Auth.GoogleSignInApi.get…nInResultFromIntent(data) must not be null
        at android.app.ActivityThread.deliverResults(ActivityThread.java:4391)
        at android.app.ActivityThread.handleSendResult(ActivityThread.java:4433)
        at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:49)
        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:1836)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6702)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911)
     Caused by: java.lang.IllegalStateException: Auth.GoogleSignInApi.get…nInResultFromIntent(data) must not be null
        at com.leapsi.sudoku.puzzle.ui.MainActivity.onActivityResult(Unknown Source:15)
        at android.app.Activity.dispatchActivityResult(Activity.java:7471)
        at android.app.ActivityThread.deliverResults(ActivityThread.java:4384)
        at android.app.ActivityThread.handleSendResult(ActivityThread.java:4433) 
        at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:49) 
        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:1836) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:193) 
        at android.app.ActivityThread.main(ActivityThread.java:6702) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:911) 

4. error: more than one device/emulator; error: more than one device/emulator; Performing Streamed Install; adb: connect error for write: more than one device/emulator

时间:2019年6月19日15:25:38
问题描述:

F:\Downloads>adb install F:\Downloads\Sudoku-v11-1.0.7-2019-06-19-104256.apk
error: more than one device/emulator
error: more than one device/emulator
Performing Streamed Install
adb: connect error for write: more than one device/emulator

F:\Downloads>

解决办法:
可以看到提示是有多于1个设备或者模拟器连接。我确实是连着两个手机的。那么可不可以定向安装到指定的手机呢?可以的。

F:\Downloads>adb devices
List of devices attached
022MWW145M007273        device
6EJ7N18531009080        device

F:\Downloads>adb -s 6EJ7N18531009080 install F:\Downloads\Sudoku-v11-1.0.7-2019-
06-19-104256.apk
Performing Streamed Install
Success

F:\Downloads>

5. android.view.WindowManager$BadTokenException: Unable to add window android.view.ViewRootImpl$W@ad0c8fa – permission denied for this window type

时间:2019年7月19日19:09:32
原因分析:这是因为没有添加权限

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" android:maxSdkVersion="22"/>

6. 使用 addOnPreDrawListener 造成自定义 Behavior 效果错误

时间:2019年7月22日19:13:14
原因分析:代码里是这样的,通过一个自定义的 Behavior 来实现头部 ViewGroup 的底边和依赖的 View 的顶边是一致的。但实际发现头部 ViewGroup 并没有滚动。定位代码到 ViewGroup 里包含的一个自定义View,具体就是 addOnPreDrawListener 这里,代码如下:

public class BatteryPercentView extends ConstraintLayout {
private static final String TAG = BatteryPercentView.class.getSimpleName();
    private BatteryScaleView mBatteryScaleView;
    private LottieAnimationView mLavPercent;
    private ImageView mIvBatteryPercentCircle;
    private ImageView mIvBatteryPercentBg;

    public BatteryPercentView(Context context) {
        this(context,null);
    }

    public BatteryPercentView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public BatteryPercentView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        inflate(context, R.layout.view_battery_percent_view, this);
        mIvBatteryPercentBg = findViewById(R.id.ivBatteryPercentBg);
        mBatteryScaleView = findViewById(R.id.batteryScaleView);
        mIvBatteryPercentCircle = findViewById(R.id.ivBatteryPercentCircle);
        mLavPercent = findViewById(R.id.lavPercent);
        //获得ViewTreeObserver
        ViewTreeObserver observer = mIvBatteryPercentBg.getViewTreeObserver();
        //注册观察者,监听变化
        observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
            @Override
            public boolean onPreDraw() {
                int width = mIvBatteryPercentBg.getWidth();
                Log.d(TAG, "onPreDraw: width = " + width);
                ConstraintLayout.LayoutParams layoutParams = (ConstraintLayout.LayoutParams) mBatteryScaleView.getLayoutParams();
                int margin = (int) (width / 2 * 0.3f);
                layoutParams.leftMargin = margin;
                layoutParams.topMargin = margin;
                layoutParams.rightMargin = margin;
                layoutParams.bottomMargin = margin;
                mBatteryScaleView.setLayoutParams(layoutParams);
                if(observer.isAlive()){
                    observer.removeOnPreDrawListener(this);
                }
                return true;
            }
        });
    }
}

在这里面,我通过 addOnPreDrawListener 监听来获取布局里图片的宽,进一步设置另一个 View 的布局参数。
另外添加 addOnGlobalLayoutListener 监听,并打印日志。查看日志发现 addOnPreDrawListener 监听会多次回调,而 addOnGlobalLayoutListener 监听只会回调一次。
解决办法:使用 addOnGlobalLayoutListener 来获取宽高,重新测试,实现了所需要的效果。

7. 使用 BottomSheetBehavior,但布局里包含横向的 RecyclerView,导致效果不正确。

时间:2019年7月22日19:43:08
问题描述:
三个色块部分是一个横向的 RecyclerView,它包含在底部的布局里面。但是,向上滑动 RecyclerView 部分,却没有把底部布局弹出,而是钻进了顶部布局下面。而滑动 RecyclerView 下边的部分,却是正常的。可以看效果图:

问题分析:参考 https://www.jianshu.com/p/5eb471950a92 这篇文字,因为 RecyclerView 的 nestedScrollingEnabled 默认是 true,通过

  public void setNestedScrollingEnabled(boolean enabled) {
        getScrollingChildHelper().setNestedScrollingEnabled(enabled);
    }

设置为 false。
查看效果:

解决了这个问题。

8. 使用 foreach 遍历集合,在里面调用了集合的 remove 方法

时间:2019年7月27日18:06:21
问题描述:
下面是示例代码:

public class Test {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("a1");
        list.add("a2");
        list.add("a3");
        list.add("a4");
        list.add("a5");
        for (String s : list) {
            if (list.indexOf(s) == 0) {
                list.remove(s);
                break;
            }
        }
        System.out.println(list);
    }
}

9. 使用 Room 的 @ForeignKey 关联一张表时,删除主表的条目,关联的表对应的条目也被删除了。

时间:2019年7月27日19:19:03
问题描述:
使用的代码如下:

@Entity(foreignKeys = {
                @ForeignKey(entity = VideoInfo.class,
                        parentColumns = "task_id",
                        childColumns = "video_task_id",
                        onDelete = ForeignKey.CASCADE)},
        indices = {@Index(value = "video_task_id")
        })
public class VideoDetailInfo {}

当我删除了 VideoInfo 中的一条数据时,这个 VideoDetailInfo 对应的条目也被删除了。由于自己不懂,就问了一下同事,默认是不会删除的。后来另外一个同事查看了 ForeignKey.CASCADE 的定义,是级联操作。参考资料:https://www.sqlite.org/foreignkeys.html。

10. android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787)

时间:2019年7月29日11:55:51
问题描述:可以看下边的日志

2019-07-29 11:56:04.339 6995-7948/ E/AndroidRuntime: FATAL EXCEPTION: pool-8-thread-2
    android.database.sqlite.SQLiteConstraintException: FOREIGN KEY constraint failed (code 787)
        at android.database.sqlite.SQLiteConnection.nativeExecuteForLastInsertedRowId(Native Method)
        at android.database.sqlite.SQLiteConnection.executeForLastInsertedRowId(SQLiteConnection.java:782)
        at android.database.sqlite.SQLiteSession.executeForLastInsertedRowId(SQLiteSession.java:788)
        at android.database.sqlite.SQLiteStatement.executeInsert(SQLiteStatement.java:86)
        at androidx.sqlite.db.framework.FrameworkSQLiteStatement.executeInsert(FrameworkSQLiteStatement.java:51)
        at androidx.room.EntityInsertionAdapter.insert(EntityInsertionAdapter.java:97)
        at com.adupsihk.downloader.pro.db.VideoDetailInfoDao_Impl.insert(VideoDetailInfoDao_Impl.java:91)
        at com.adupsihk.downloader.pro.db.AppDataBaseHelper.insertDetail(AppDataBaseHelper.java:143)
        at com.adupsihk.downloader.pro.utils.DownloadManager$1$1.run(DownloadManager.java:139)
        at com.adupsihk.downloader.pro.utils.ThreadPool$TaskWithPriority.run(ThreadPool.java:173)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
        at java.lang.Thread.run(Thread.java:760)

原因分析:
问题指向了代码:

                        AppDataBaseHelper.insert(newVideoInfos); // 这个是主表集合的插入
                        AppDataBaseHelper.insertDetail(newVideoDetailInfos); // 这个是附表集合的插入

我是使用 Room 来操作数据库的,查看了一个 Entity 部分的写法,是没有问题的。
找同事帮忙看,就 debug 来看,在上面第二行代码处打断点,却发现上面一行存入的集合大小是 0,

原来是自己上面忘记了添加数据到 newVideoInfos,但添加了数据到 newVideoDetailInfos。这样,给主表就没有添加数据,但却给附表添加了数据。附表没有在主表里找到对应的外键数据,就报错了。

最后

代码出错了,关键是要仔细查看日志。能够仔细地查看日志,就离解决问题很近了。

猜你喜欢

转载自blog.csdn.net/willway_wang/article/details/91604164