首页 > 其他分享 >【Android面试八股文】为什么Android中要设计为只能在UI线程中去更新UI?Android中子线程真的不能更新UI吗?如何将子线程的任务结果传递到UI线程进行更新?

【Android面试八股文】为什么Android中要设计为只能在UI线程中去更新UI?Android中子线程真的不能更新UI吗?如何将子线程的任务结果传递到UI线程进行更新?

时间:2024-06-13 16:28:52浏览次数:13  
标签:void 更新 UI new 线程 Android public

文章目录

一、Android为什么不能在子线程更新UI?

viewRootImpl对象是在Activity中的onResume方法执行完成之后,View变得可见时才创建的,之前的操作是没有进行线程检查的,所以没有报错。但是ViewRootImpl创建之后,由于进行了checkThread操作,所以就不能在子线程更改UI了。

当访问 UI 时,ViewRootImpl 会调用 checkThread方法去检查当前访问 UI 的线程是否为创建 UI 的那个线程,如果不是。则会抛出异常。

当然可以,从系统源码的角度来解释为什么 Android 中子线程不能直接更新 UI。

  1. ViewRootImpl 的创建

在 Android 应用的生命周期中,ViewRootImpl 对象是在 Activity 的视图变得可见时被创建的。ViewRootImpl 是负责管理视图层次结构、处理测量、布局和绘制的核心类。

ActivityThread.java 中的 handleResumeActivity 方法是一个关键点:

public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
    // ...省略部分代码...

    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        ViewManager wm = a.getWindowManager();
        WindowManager.LayoutParams l = r.window.getAttributes();
        a.mDecor = decor;
        wm.addView(decor, l);
        // 在这里创建 ViewRootImpl 并关联 DecorView
    }
    
    // ...省略部分代码...
}

handleResumeActivity 方法中,当 Activity 被恢复到前台时,会创建 ViewRootImpl 并关联到 DecorView(根视图)。

  1. checkThread 方法

ViewRootImpl 内部,每当你尝试操作 UI 时,都会调用 checkThread 方法来检查当前线程是否为主线程。

ViewRootImpl.java 中的 checkThread 方法:

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

checkThread 方法会比较当前线程 Thread.currentThread() 和创建 ViewRootImpl 时的线程 mThread。如果它们不相同,则抛出 CalledFromWrongThreadException 异常。

  1. 触发 checkThread 检查的示例

当你在子线程中尝试更新 UI 时,例如通过调用 TextView.setText 方法,就会触发上述检查。以下是一个简化版本的示例:

public void setText(CharSequence text) {
    // ...省略部分代码...
    checkForThreadViolation();
    // ...省略部分代码...
}

private void checkForThreadViolation() {
    if (mDispatchSystemThreadChecks && Looper.myLooper() != mViewRootImpl.mThread.getLooper()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

setText 方法中,会调用 checkForThreadViolation 来检查当前线程是否为主线程。如果不是,则抛出 CalledFromWrongThreadException

  1. 小结

从系统源码的角度来看,Android 强制 UI 操作在主线程中进行的原因主要包含以下几个方面:

  1. ViewRootImpl 的创建

    • ViewRootImplActivity 前台显示时创建,并关联到视图层次结构中。
  2. checkThread 方法

    • ViewRootImpl 内部的 checkThread 方法会检查所有 UI 操作是否在主线程进行。
    • 如果检测到非主线程操作 UI,就会抛出 CalledFromWrongThreadException 异常。

这种设计确保了 UI 操作的安全性和一致性,因为 Android 的视图系统并不是线程安全的,在多线程操作下可能会导致不可预期的行为或崩溃。因此,强制在主线程中操作 UI 是保证应用稳定性的重要机制。


二、为什么Android中要设计为只能在UI线程中去更新UI?

面试官: “为什么Android中要设计为只能在UI线程中去更新UI呢?”

面试者: “这是为了保证UI操作的线程安全性和一致性。具体来说,有以下几个原因:”

  1. 线程安全:

    • Android的UI组件并不是线程安全的。如果多个线程同时操作同一个UI组件,可能会导致数据竞争、死锁等问题,造成应用程序的不稳定甚至崩溃。
  2. 一致性和响应性:

    • 主线程负责处理用户输入、绘制界面等操作。如果允许在子线程中更新UI,可能会导致绘制过程和用户交互的不一致,出现界面闪烁、控件状态异常等问题。将所有UI更新操作限制在主线程,可以确保用户界面的状态和操作的一致性。
  3. 绘制机制:

    • Android的UI绘制机制是单线程的,主线程负责周期性地刷新界面。如果子线程直接修改UI,可能会在绘制过程中打断绘制流程,导致界面显示错误或绘制不完整。
  4. 简化开发:

    • 强制在主线程更新UI,可以简化开发者的工作,避免开发者自己处理复杂的线程同步问题,从而减少Bug的产生,提高应用的稳定性和可维护性。

三、如果不在UI线程中更新UI,可能会出现什么问题呢?

面试官: “如果不在UI线程中更新UI,可能会出现什么问题呢?”

面试者: “如果在非UI线程中更新UI,可能会出现以下问题:”

  1. 界面更新不及时:

    • 由于绘制和更新操作可能被打断,界面可能不会及时反映最新的状态,用户体验会变差。
  2. 应用崩溃:

    • 非UI线程操作UI组件会导致数据竞争等问题,可能会触发异常,导致应用崩溃。
  3. 界面闪烁或不稳定:

    • 多线程操作UI会造成绘制过程不一致,导致界面闪烁、控件状态异常等问题。

四、ViewRootImp是在onActivityCreated方法后面创建的吗?

实际上,ViewRootImpl 并不是在 onActivityCreated 方法后创建的。它是在 Activity 的视图变得可见时被创建的。让我们来看一下 Activity 的启动过程中关于 ViewRootImpl 的创建时机。

  1. 当调用 startActivity 启动一个新的 Activity 时,系统会执行一系列的生命周期方法,包括 onCreateonStartonResume 等。

  2. onCreate 方法中,通常会设置布局资源并通过 setContentView 方法加载布局文件,这时候并不会立即创建 ViewRootImpl

  3. Activity 进入前台,即将对用户可见时,系统会调用 onResume 方法。在 onResume 方法中,会进行一系列的准备工作,包括创建 ViewRootImpl 对象并关联到 DecorView 上,从而使得整个视图层次结构能够与窗口管理器进行交互。

因此,ViewRootImpl 的创建时机应该是在 onResume 方法中,在 Activity 进入前台并将对用户可见时。这时候才会触发 ViewRootImpl 的创建和与 DecorView 的关联,从而开始处理视图的测量、布局、绘制等操作。

五、为什么一开始在Activity的onCreate方法中创建一个子线程访问UI,程序还是正常能跑起来呢

Activity 的生命周期中,虽然严格来说 UI 操作应该在主线程中进行,但在某些情况下,比如在 onCreate 方法中立即创建子线程并访问 UI 元素,程序可能不会立刻崩溃。这是因为在 onCreate 方法中,视图层次结构还没有完全初始化完毕。让我们来分析这个现象。

1. Activity 生命周期概述

当一个 Activity 被创建时,系统会依次调用以下生命周期方法:

  1. onCreate
  2. onStart
  3. onResume

2. 视图层次结构的初始化

onCreate 方法中,通常会调用 setContentView 来设置布局文件:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    // 在这里创建子线程并访问UI
}

setContentView 会将 XML 布局文件解析成对应的视图对象,并添加到 Activity 的视图层次结构中,但此时视图层次结构还没有完全初始化和展示。

3. 子线程访问 UI 的时机

如果你在 onCreate 方法中创建子线程并立即访问 UI 元素,例如:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    new Thread(new Runnable() {
        @Override
        public void run() {
            TextView textView = findViewById(R.id.textView);
            textView.setText("Hello from thread");
        }
    }).start();
}

这种情况下,子线程很可能在视图层次结构完全初始化之前就尝试访问 UI 元素。由于 ViewRootImpl 还没有完全创建或关联到视图树上,checkThread 检查可能尚未触发。因此,虽然这段代码理论上是不安全的,但在特定时机下不会立刻引发异常。

4. 潜在问题

尽管上述情况可能不会立刻导致崩溃,但它仍然是不安全和错误的做法。任何在子线程中访问 UI 的操作都有可能导致难以调试的竞态条件和不一致性问题。一旦视图层次结构初始化完成,后续在子线程中访问 UI 将触发 ViewRootImpl 中的线程检查机制,抛出 CalledFromWrongThreadException 异常。

正确的做法

为了确保线程安全,所有的 UI 更新操作都应该在主线程中进行。可以使用 HandlerrunOnUiThread 方法在主线程中执行 UI 更新,例如:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    new Thread(new Runnable() {
        @Override
        public void run() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    TextView textView = findViewById(R.id.textView);
                    textView.setText("Hello from thread");
                }
            });
        }
    }).start();
}

这样可以确保所有的 UI 操作都是在主线程中进行的,避免了潜在的多线程问题。

六、Android中子线程真的不能更新UI吗?

在 Android 开发中,我们经常听到一句话:“只能在主线程(UI线程)中更新UI”。但实际上,任何线程都可以更新自己创建的UI,只是需要满足特定的条件:

  1. ViewRootImpl 创建之前

ViewRootImpl(负责管理视图树的根视图)创建之前,可以随意在任何线程中修改 UI,因为此时不会进行线程检查(即不会执行 checkThread 方法)。

  1. ViewRootImpl 创建之后

一旦 ViewRootImpl 创建完成后,你必须保证“创建 ViewRootImpl 的操作”和“更新 UI 的操作”在同一个线程中。换句话说,需要在同一个线程中调用 ViewManageraddViewupdateViewLayout 方法。

注:ViewManager 是一个接口,WindowManager 接口继承了它。我们通常通过 WindowManager(具体实现为 WindowManagerImpl)来进行视图的添加、删除和更新操作。

此外,在对应的线程中,还需要创建 Looper 并调用 Looperloop 方法,开启消息循环,使得该线程能够处理消息队列中的消息。

示例代码

下面是一个简单的示例,展示如何在子线程中更新UI。

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    private ViewGroup rootLayout;
    private static final int UPDATE_UI = 1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        rootLayout = findViewById(R.id.root_layout);

        // 创建一个子线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 创建一个 Looper 并启动消息循环
                Looper.prepare();

                // 创建一个 Handler 用于在子线程中更新UI
                Handler handler = new Handler(Looper.myLooper()) {
                    @Override
                    public void handleMessage(@NonNull Message msg) {
                        if (msg.what == UPDATE_UI) {
                            // 更新 UI
                            TextView textView = new TextView(MainActivity.this);
                            textView.setText("Hello from the child thread!");
                            rootLayout.addView(textView);
                        }
                    }
                };

                // 发送更新 UI 的消息
                handler.sendEmptyMessage(UPDATE_UI);

                // 开启消息循环
                Looper.loop();
            }
        }).start();
    }
}

在这个示例中,我们在 onCreate 方法中启动了一个子线程。在子线程中,我们创建了一个 Looper 并启动了消息循环。然后,我们使用 Handler 在子线程中发送消息并更新 UI。

通过这种方式,我们可以确保在子线程中安全地更新UI。但通常情况下,为了避免复杂性和潜在的错误,建议还是在主线程中操作UI。


希望这个版本能帮助您更好地理解在 Android 中子线程更新 UI 的问题。

七、在实际开发中,如何将子线程的任务结果传递到UI线程进行更新?

面试官: “你刚才提到的原因都很清楚。那你能说一下,在实际开发中,如何将子线程的任务结果传递到UI线程进行更新吗?”

面试者: “当然,以下是几种常见的方法:”

  1. new Handler()
Button button = new Button(this);

Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        if (msg.what == 1) {
          button.setText("子线程更新UI");
        }
    }
};


new Thread(new Runnable() {
    @Override
    public void run() {
        // Message和Handler均可获得msg
        // Message msg = handler.obtainMessage();
        Message msg = Message.obtain();
        msg.what = 1;
        msg.arg1 = 10;
        handler.sendMessage(msg);
    }
}).start();

  1. new Handler.Callback()
Button button = new Button(this);

private Handler.Callback callback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
    	if (msg.what == 1) {
        	button.setText("子线程更新UI");
        }
        return true;
    }
};

Handler handler = new Handler(callback);

new Thread(new Runnable() {
    @Override
    public void run() {
    	// Message和Handler均可获得msg
        // Message msg = handler.obtainMessage();
        Message msg = Message.obtain();
        msg.what = 1;
        msg.arg1 = 11;
        handler.sendMessage(msg);
    }
}).start();

  1. new Handler().post(Runnable r)

    • Handler可以将任务从子线程传递到主线程,在主线程中处理消息。
    Handler handler = new Handler(Looper.getMainLooper());
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 执行后台任务
            final String result = performTask();
    
            // 在主线程更新UI
            handler.post(new Runnable() {
                @Override
                public void run() {
                    textView.setText(result);
                }
            });
        }
    }).start();
    
  2. new Handler().postDelayed(Runnable r, long delayMillis)

    Handler handler = new Handler(Looper.getMainLooper());
    new Thread(new Runnable() {
        @Override
        public void run() {
            // 执行后台任务
            final String result = performTask();
    
            // 在主线程更新UI
            handler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    textView.setText(result);
                }
            }, 3000);
        }
    }).start();
    
  3. 使用Activity.runOnUiThread()

    • 直接在主线程执行更新UI的代码。
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            textView.setText("更新后的文本");
        }
    });
    
  4. 使用View.post()

    • 将任务提交到View的消息队列,在主线程执行。
    textView.post(new Runnable() {
        @Override
        public void run() {
            textView.setText("更新后的文本");
        }
    });
    
  5. 使用View.post()

    • 将任务提交到View的消息队列,在主线程执行。
    textView.postDelay(new Runnable() {
        @Override
        public void run() {
            textView.setText("更新后的文本");
        }
    },3000);
    
  6. 使用AsyncTask

    • 在后台线程执行任务,并在主线程更新UI。
    new AsyncTask<Void, Void, String>() {
        @Override
        protected String doInBackground(Void... params) {
            // 执行后台任务
            return performTask();
        }
    
        @Override
        protected void onPostExecute(String result) {
            // 在主线程更新UI
            textView.setText(result);
        }
    }.execute();
    
  7. EventBus
    定义一个事件类:

    // 定义一个事件类
    public class MessageEvent {
        public final String message;
    
        public MessageEvent(String message) {
            this.message = message;
        }
    }
    

    在主活动(或任何需要更新UI的地方)中,注册和接收事件:

    import android.os.Bundle;
    import android.widget.TextView;
    import androidx.appcompat.app.AppCompatActivity;
    import org.greenrobot.eventbus.EventBus;
    import org.greenrobot.eventbus.Subscribe;
    import org.greenrobot.eventbus.ThreadMode;
    
    public class MainActivity extends AppCompatActivity {
    
        private TextView textView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            textView = findViewById(R.id.text_view);
    
            // 注册EventBus
            EventBus.getDefault().register(this);
    
            // 模拟子线程发布事件
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(2000); // 模拟耗时操作
                        EventBus.getDefault().post(new MessageEvent("Hello from background thread!"));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    
        @Subscribe(threadMode = ThreadMode.MAIN)
        public void onMessageEvent(MessageEvent event) {
            // 在主线程中接收事件并更新UI
            textView.setText(event.message);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            // 取消注册EventBus
            EventBus.getDefault().unregister(this);
        }
    }
    

    在这个例子中:

    1. 定义事件类:我们定义了一个简单的MessageEvent类,用于传递字符串消息。
    2. 注册EventBus:在MainActivityonCreate方法中,我们使用EventBus.getDefault().register(this)来注册EventBus。
    3. 发布事件:在一个子线程中,我们通过EventBus.getDefault().post(new MessageEvent("..."))来发布事件。
    4. 接收事件:使用@Subscribe注解来标记一个方法,这个方法就是用来接收事件的。在这里,我们指定threadMode = ThreadMode.MAIN,这表示该方法将在主线程中执行,从而可以安全地更新UI。
    5. 取消注册:在onDestroy方法中,我们使用EventBus.getDefault().unregister(this)来取消注册,以避免内存泄漏。

    通过这种方式,我们可以轻松地在子线程中发布事件,并在主线程中接收并处理这些事件,从而安全地更新UI。

  8. Rxjava

Disposable disposable = Observable.fromCallable(new Callable<String>() {
    @Override
    public String call() throws Exception {
        return performTask();
    }
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<String>() {
    @Override
    public void accept(String result) throws Exception {
        textView.setText(result);
    }
});

// 记得在适当的时候释放资源
disposable.dispose();

面试官: “非常好,你对为什么只能在UI线程更新UI以及如何在实际开发中实现这一点有很清楚的理解。你的回答非常完整。”

七、使用子线程更新UI有实际应用场景吗

Android 中的 SurfaceView 通常会通过一个子线程来进行页面的刷新。

如果我们的自定义 View 需要频繁刷新,或者刷新时数据处理量比较大,那么可以考虑使用 SurfaceView 来取代 View

SurfaceView 是 Android 中用于实现高效绘图的视图组件,它适合于需要频繁刷新或进行复杂绘制操作的场景。与普通的 View 不同,SurfaceView 提供了一个独立的绘图表面,这个表面可以在独立的线程上进行绘制操作。

SurfaceView 刷新UI的机制

  • 独立绘图表面SurfaceView 创建了一个独立的绘图表面(Surface),这个表面可以直接由子线程来访问和更新。这是 SurfaceView 的关键特性,它允许在不影响主线程(UI线程)的情况下进行绘图操作。

  • SurfaceHolderSurfaceView 通过 SurfaceHolder 提供对其绘图表面的访问接口。SurfaceHolder 提供了一系列的方法来锁定和解锁画布,用于在独立的线程上进行绘制。

使用子线程刷新UI的示例

  1. 定义 SurfaceView 子类
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
    private SurfaceHolder surfaceHolder;
    private DrawingThread drawingThread;

    public MySurfaceView(Context context) {
        super(context);
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        drawingThread = new DrawingThread(surfaceHolder);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        drawingThread.setRunning(true);
        drawingThread.start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        // Handle changes here
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;
        drawingThread.setRunning(false);
        while (retry) {
            try {
                drawingThread.join();
                retry = false;
            } catch (InterruptedException e) {
                // Retry stopping the thread
            }
        }
    }
}
  1. 定义绘图线程
class DrawingThread extends Thread {
    private SurfaceHolder surfaceHolder;
    private boolean running;

    public DrawingThread(SurfaceHolder surfaceHolder) {
        this.surfaceHolder = surfaceHolder;
        running = false;
    }

    public void setRunning(boolean running) {
        this.running = running;
    }

    @Override
    public void run() {
        while (running) {
            Canvas canvas = null;
            try {
                canvas = surfaceHolder.lockCanvas();
                synchronized (surfaceHolder) {
                    // Perform drawing operations on the canvas here
                    canvas.drawColor(Color.BLACK); // Example: Clear the canvas with black color
                }
            } finally {
                if (canvas != null) {
                    surfaceHolder.unlockCanvasAndPost(canvas);
                }
            }
        }
    }
}

主要要点

  • SurfaceHolder.CallbackMySurfaceView 实现了 SurfaceHolder.Callback 接口,以便接收 SurfaceView 表面创建、变更和销毁的事件。这些回调方法用于控制绘图线程的启动和停止。

  • DrawingThread:绘图线程 (DrawingThread) 在独立的线程中运行,避免阻塞主线程。在 run 方法中,线程会不断地锁定画布进行绘制,然后解锁并提交绘制结果。

  • 线程控制:在 surfaceCreated 方法中启动绘图线程,在 surfaceDestroyed 方法中停止绘图线程。这确保了在 SurfaceView 被创建和销毁时正确地管理线程的生命周期。

通过这种方式,SurfaceView 可以利用子线程进行UI的刷新操作,从而提高绘图的效率,减少对主线程的干扰。不过,需要注意的是,在进行多线程绘图时,要小心处理线程同步问题,以避免竞争和死锁等问题。

八、扩展阅读

Android为什么不能在子线程更新UI

标签:void,更新,UI,new,线程,Android,public
From: https://blog.csdn.net/qq446282412/article/details/139656264

相关文章

  • 【Android面试八股文】我们来聊一聊IdelHandler吧,IdelHandler能干什么?怎么使用?有什么
    文章目录一、简单说说Handler机制二、IdleHandler是什么?怎么用?三、什么时候出现空闲时间可以执行IdleHandler四、IdleHander是如何保证不进入死循环的?五、你知道在Framework中如何使用IdleHander?六、一些其他面试问题Handler机制算是Android基本功,面试......
  • 数据库 android 数据库软件
    Access数据库    Access数据库被集成在Office办公软件中,是世界上最流行的桌面数据库管理系统。Access是一种功能强大且使用方便的关系型数据库管理系统,一般也称关系型数据库管理软件。它可运行于各种MicrosoftWindows系统环境中,由于它继承了Windows的特性,不仅易于使用,而......
  • 界面控件DevExpress WinForms垂直&属性网格组件 - 拥有更灵活的UI选择(一)
    DevExpressWinForms垂直&属性网格组件旨在提供UI灵活性,它允许用户显示数据集中的单个行或在其90度倒置网格容器中显示多行数据集。另外,用户可以把它用作一个属性网格,就像在VisualStudioIDE中那样。P.S:DevExpressWinForms拥有180+组件和UI库,能为WindowsForms平台创建具有......
  • School cleaning equipment standard package
    Sweepercleaningequipmentisdividedintohand-pushsweepersandride-onsweepers.Thismachinesweepsawaydustwhilesweepingthefloor,one-step,efficientcleaning,savinglaborcostsandimprovingcleaningefficiency.Hand-pushsweepersaredivided......
  • webman admin 中的 Layui 使用说明
    目录Layui背景生命周期底层方法通用Common模块化相关Modular事件Event模块系统入门Layui模块系统与vue模块的区别理解layui.use代码解释Layui背景Layui是一个上古框架,jQuery时代末期Vue初期的作品,早期因为layout.js弹出层起家,在作者的努力下,成体系的开发了一套常......
  • Future集合会等线程池执行完才开始遍历吗?
    先说结论:Future集合并不是等线程池执行完才开始遍历,而是线程池内的线程执行完一条Future集合就立即遍历一条在使用线程池的业务场景下,我们经常需要获取线程执行的返回值,此时我们需要Callable对象当做线程池参数并用List<Future>接收,然后遍历List<Future>获取我们想要的值。但是......
  • stable-diffusion-webui 环境配置
    链接:AUTOMATIC1111/stable-diffusion-webui:StableDiffusionwebUI(github.com)查看python版本:   安装完python之后,打开工程,配置虚拟环境 下载安装对应的依赖库,其中的torch相关三个库选择与自己cuda相关的版本安装,我的pytorch选的是2.1.2,下载网址:download.pytorch.o......
  • C#中使用AutoResetEvent或者ManualResetEvent做线程管理
    1.Task/thread/sync/async..await/WhenAll相关基础知识参见此处链接2.什么是AutoResetEvent和ManualResetEvent事件他们都是C#中System.Threading下面的类。用于在多个线程之间进行事件通知和管理。他们的管理方法主要是三个:Reset():关闭WaitOne():阻挡Set():启动AutoR......
  • Android studio 自动复制生成的 apk
    KTS脚本//顶层build.gradle.ktsvalapkBaseName:String="VoiceAssistant"allprojects{project.extra.apply{set("apkBaseName",apkBaseName)}}//模块build.gradle.ktsandroid{ applicationVariants.all{......
  • This version of the Android Support plugin for IntelliJ IDEA or Android Studio c
    解决低版本的android导入高版本的工程7.2修改适配android4.2.11、setting.gradle保留rootProject.name=""和include‘:app’,其余注释//pluginManagement{//repositories{//gradlePluginPortal()//google()//mavenCentral()//}//......