ChatGPT解决这个技术问题 Extra ChatGPT

Android“只有创建视图层次结构的原始线程才能触摸它的视图。”

我在 Android 中构建了一个简单的音乐播放器。每首歌曲的视图包含一个 SeekBar,实现如下:

public class Song extends Activity implements OnClickListener,Runnable {
    private SeekBar progress;
    private MediaPlayer mp;

    // ...

    private ServiceConnection onService = new ServiceConnection() {
          public void onServiceConnected(ComponentName className,
            IBinder rawBinder) {
              appService = ((MPService.LocalBinder)rawBinder).getService(); // service that handles the MediaPlayer
              progress.setVisibility(SeekBar.VISIBLE);
              progress.setProgress(0);
              mp = appService.getMP();
              appService.playSong(title);
              progress.setMax(mp.getDuration());
              new Thread(Song.this).start();
          }
          public void onServiceDisconnected(ComponentName classname) {
              appService = null;
          }
    };

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.song);

        // ...

        progress = (SeekBar) findViewById(R.id.progress);

        // ...
    }

    public void run() {
    int pos = 0;
    int total = mp.getDuration();
    while (mp != null && pos<total) {
        try {
            Thread.sleep(1000);
            pos = appService.getSongPosition();
        } catch (InterruptedException e) {
            return;
        } catch (Exception e) {
            return;
        }
        progress.setProgress(pos);
    }
}

这工作正常。现在我想要一个计时器来计算歌曲进度的秒数/分钟数。所以我在布局中放置了一个 TextView,在 onCreate() 中使用 findViewById() 获取它,然后将它放在 progress.setProgress(pos) 之后的 run() 中:

String time = String.format("%d:%d",
            TimeUnit.MILLISECONDS.toMinutes(pos),
            TimeUnit.MILLISECONDS.toSeconds(pos),
            TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(
                    pos))
            );
currentTime.setText(time);  // currentTime = (TextView) findViewById(R.id.current_time);

但是最后一行给了我一个例外:

android.view.ViewRoot$CalledFromWrongThreadException:只有创建视图层次结构的原始线程才能触摸其视图。

然而,我在这里做的事情与我对 SeekBar 所做的基本相同 - 在 onCreate 中创建视图,然后在 run() 中触摸它 - 它并没有给我这个抱怨。


P
Peter Mortensen

您必须将更新 UI 的后台任务部分移动到主线程上。为此有一段简单的代码:

runOnUiThread(new Runnable() {

    @Override
    public void run() {

        // Stuff that updates the UI

    }
});

Activity.runOnUiThread 的文档。

只需将其嵌套在后台运行的方法中,然后将实现任何更新的代码复制粘贴到块的中间。尽可能只包含最少量的代码,否则你会开始破坏后台线程的目的。


像魅力一样工作。对我来说,这里唯一的问题是我想在 run() 方法中做一个 error.setText(res.toString()); ,但我不能使用 res 因为它不是最终的......太糟糕了
对此有一个简短的评论。我有一个单独的线程试图修改 UI,上面的代码有效,但我从 Activity 对象调用了 runOnUiThread。我不得不做myActivityObject.runOnUiThread(etc)之类的事情
@Kirby 感谢您提供此参考。您可以简单地执行“MainActivity.this”,它应该也可以正常工作,因此您不必保留对您的活动类的引用。
我花了一段时间才弄清楚 runOnUiThread() 是 Activity 的一种方法。我在一个片段中运行我的代码。我最终做了getActivity().runOnUiThread(etc),它奏效了。极好的!;
我们可以停止正在执行的“runOnUiThread”方法主体中执行的任务吗?
S
Seth

我通过将 runOnUiThread( new Runnable(){ .. 放入 run() 解决了这个问题:

thread = new Thread(){
        @Override
        public void run() {
            try {
                synchronized (this) {
                    wait(5000);

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            dbloadingInfo.setVisibility(View.VISIBLE);
                            bar.setVisibility(View.INVISIBLE);
                            loadingText.setVisibility(View.INVISIBLE);
                        }
                    });

                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Intent mainActivity = new Intent(getApplicationContext(),MainActivity.class);
            startActivity(mainActivity);
        };
    };  
    thread.start();

这个震动了。感谢您提供的信息,这也可以在任何其他线程中使用。
谢谢,为了回到 UI 线程而创建一个线程真的很难过,但只有这个解决方案才救了我的案子。
一个重要方面是 wait(5000); 不在 Runnable 中,否则您的 UI 将在等待期间冻结。对于此类操作,您应该考虑使用 AsyncTask 而不是 Thread。
这对内存泄漏来说太糟糕了
为什么要打扰同步块?它里面的代码看起来相当线程安全(尽管我已经完全准备好接受我的话)。
P
Peter Mortensen

我对此的解决方案:

private void setText(final TextView text,final String value){
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            text.setText(value);
        }
    });
}

在后台线程上调用此方法。


错误:(73, 67) 错误:无法从静态上下文引用非静态方法集(字符串)
我的测试课程也有同样的问题。这对我来说就像一个魅力。但是,将 runOnUiThread 替换为 runTestOnUiThread。谢谢
K
KenIchi

Kotlin 协程可以让你的代码更简洁易读,如下所示:

MainScope().launch {
    withContext(Dispatchers.Default) {
        //TODO("Background processing...")
    }
    TODO("Update UI here!")
}

或相反亦然:

GlobalScope.launch {
    //TODO("Background processing...")
    withContext(Dispatchers.Main) {
        // TODO("Update UI here!")
    }
    TODO("Continue background processing...")
}

完美的!谢谢!
非常感谢你拯救了我的一天
感谢您节省了我的时间。
h
hoss

通常,任何涉及用户界面的操作都必须在主线程或 UI 线程中完成,即执行 onCreate() 和事件处理的线程。一种确定方法是使用 runOnUiThread(),另一种方法是使用处理程序。

ProgressBar.setProgress() 有一种机制,它总是在主线程上执行,所以这就是它起作用的原因。

请参阅Painless Threading


该链接上的 Painless Threading 文章现在是 404。这是一个(旧的?)关于 Painless Threading 的博客文章的链接 - android-developers.blogspot.com/2009/05/painless-threading.html
J
Jonathan

我一直处于这种情况,但我找到了处理程序对象的解决方案。

就我而言,我想用 observer pattern 更新 ProgressDialog。我的视图实现了观察者并覆盖了更新方法。

因此,我的主线程创建视图,另一个线程调用更新 ProgressDialop 的 update 方法和....:

只有创建视图层次结构的原始线程才能接触其视图。

处理程序对象可以解决问题。

下面,我的代码的不同部分:

public class ViewExecution extends Activity implements Observer{

    static final int PROGRESS_DIALOG = 0;
    ProgressDialog progressDialog;
    int currentNumber;

    public void onCreate(Bundle savedInstanceState) {

        currentNumber = 0;
        final Button launchPolicyButton =  ((Button) this.findViewById(R.id.launchButton));
        launchPolicyButton.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                showDialog(PROGRESS_DIALOG);
            }
        });
    }

    @Override
    protected Dialog onCreateDialog(int id) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog = new ProgressDialog(this);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
            progressDialog.setMessage("Loading");
            progressDialog.setCancelable(true);
            return progressDialog;
        default:
            return null;
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
        switch(id) {
        case PROGRESS_DIALOG:
            progressDialog.setProgress(0);
        }

    }

    // Define the Handler that receives messages from the thread and update the progress
    final Handler handler = new Handler() {
        public void handleMessage(Message msg) {
            int current = msg.arg1;
            progressDialog.setProgress(current);
            if (current >= 100){
                removeDialog (PROGRESS_DIALOG);
            }
        }
    };

    // The method called by the observer (the second thread)
    @Override
    public void update(Observable obs, Object arg1) {

        Message msg = handler.obtainMessage();
        msg.arg1 = ++currentPluginNumber;
        handler.sendMessage(msg);
    }
}

此说明可在 this page 上找到,您必须阅读“带有第二个线程的示例 ProgressDialog”。


V
Vivek Thummar

您可以使用 Handler 来删除视图,而不会干扰主 UI 线程。这是示例代码

new Handler(Looper.getMainLooper()).post(new Runnable() {
    @Override
    public void run() {
          //do stuff like remove view etc
          adapter.remove(selecteditem);
          }
    });

C
Caner

科特林答案

我们必须以真正的方式使用 UI Thread 来完成这项工作。我们可以在 Kotlin 中使用 UI Thread,例如:

runOnUiThread(Runnable {
   //TODO: Your job is here..!
})

D
David Dimalanta

我看到你已经接受了@providence 的回答。以防万一,您也可以使用处理程序!首先,做 int 字段。

    private static final int SHOW_LOG = 1;
    private static final int HIDE_LOG = 0;

接下来,将处理程序实例作为字段。

    //TODO __________[ Handler ]__________
    @SuppressLint("HandlerLeak")
    protected Handler handler = new Handler()
    {
        @Override
        public void handleMessage(Message msg)
        {
            // Put code here...

            // Set a switch statement to toggle it on or off.
            switch(msg.what)
            {
            case SHOW_LOG:
            {
                ads.setVisibility(View.VISIBLE);
                break;
            }
            case HIDE_LOG:
            {
                ads.setVisibility(View.GONE);
                break;
            }
            }
        }
    };

做一个方法。

//TODO __________[ Callbacks ]__________
@Override
public void showHandler(boolean show)
{
    handler.sendEmptyMessage(show ? SHOW_LOG : HIDE_LOG);
}

最后,将其放在 onCreate() 方法中。

showHandler(true);

A
Allan Pereira

使用此代码,无需 runOnUiThread 功能:

private Handler handler;
private Runnable handlerTask;

void StartTimer(){
    handler = new Handler();   
    handlerTask = new Runnable()
    {
        @Override 
        public void run() { 
            // do something  
            textView.setText("some text");
            handler.postDelayed(handlerTask, 1000);    
        }
    };
    handlerTask.run();
}

P
Peter Mortensen

我有一个类似的问题,我的解决方案很丑,但它有效:

void showCode() {
    hideRegisterMessage(); // Hides view 
    final Handler handler = new Handler();
    handler.postDelayed(new Runnable() {
        @Override
        public void run() {
            showRegisterMessage(); // Shows view
        }
    }, 3000); // After 3 seconds
}

@R.jzadeh 很高兴听到这个消息。自从我写下那个答案的那一刻起,也许现在你可以做得更好:)
H
Hagbard

我遇到了类似的问题,上面提到的方法都不适合我。最后,这对我有用:

Device.BeginInvokeOnMainThread(() =>
    {
        myMethod();
    });

我找到了这颗宝石 here


P
Peter Mortensen

我将 HandlerLooper.getMainLooper() 一起使用。它对我来说很好。

    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(Message msg) {
              // Any UI task, example
              textView.setText("your text");
        }
    };
    handler.sendEmptyMessage(1);

P
Peter Mortensen

这是明确抛出错误。它说无论哪个线程创建了一个视图,只有那个可以触及它的视图。这是因为创建的视图位于该线程的空间内。视图创建 (GUI) 发生在 UI(主)线程中。因此,您总是使用 UI 线程来访问这些方法。

https://i.stack.imgur.com/rTJZq.png

在上图中,进度变量在 UI 线程的空间内。所以,只有 UI 线程可以访问这个变量。在这里,您正在通过 new Thread() 访问进度,这就是您遇到错误的原因。


P
Peter Mortensen

当我从 Asynctask 而不是使用 onPostExecute 调用从 doInBackground 更改 UI 时,这发生在我身上。

处理 onPostExecute 中的 UI 解决了我的问题。


谢谢乔纳森。这也是我的问题,但我必须做更多的阅读才能理解你在这里的意思。对于其他任何人,onPostExecute 也是 AsyncTask 的一种方法,但它在 UI 线程上运行。见这里:blog.teamtreehouse.com/all-about-android-asynctasks
P
Peter Mortensen

我正在使用一个不包含对上下文的引用的类。所以我无法使用 runOnUIThread(); 我使用了 view.post(); 并解决了。

timer.scheduleAtFixedRate(new TimerTask() {

    @Override
    public void run() {
        final int currentPosition = mediaPlayer.getCurrentPosition();
        audioMessage.seekBar.setProgress(currentPosition / 1000);
        audioMessage.tvPlayDuration.post(new Runnable() {
            @Override
            public void run() {
                audioMessage.tvPlayDuration.setText(ChatDateTimeFormatter.getDuration(currentPosition));
            }
        });
    }
}, 0, 1000);

audioMessagetvPlayDuration 与问题代码有何相似之处?
audioMessage 是文本视图的持有者对象。 tvPlayDuration 是我们要从非 UI 线程更新的文本视图。在上面的问题中,currentTime 是文本视图,但它没有持有者对象。
D
Deepak Kataria

使用 AsyncTask 时更新 onPostExecute 方法中的 UI

    @Override
    protected void onPostExecute(String s) {
   // Update UI here

     }

这发生在我身上。我正在更新异步任务的 doinbackground 中的 ui。
A
Amit Yadav

这是提到的异常的堆栈跟踪

        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6149)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:843)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.view.View.requestLayout(View.java:16474)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
        at android.view.View.requestLayout(View.java:16474)
        at android.widget.RelativeLayout.requestLayout(RelativeLayout.java:352)
        at android.view.View.setFlags(View.java:8938)
        at android.view.View.setVisibility(View.java:6066)

所以如果你去挖掘然后你就会知道

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

mThread 在构造函数中初始化的位置,如下所示

mThread = Thread.currentThread();

我的意思是说,当我们创建特定视图时,我们在 UI 线程上创建它,然后尝试在工作线程中进行修改。

我们可以通过下面的代码片段来验证它

Thread.currentThread().getName()

当我们膨胀布局时,稍后您会遇到异常。


P
Peter Mortensen

如果您不想使用 runOnUiThread API,您实际上可以为需要几秒钟才能完成的操作实现 AsynTask。但在这种情况下,同样在 doinBackground() 中处理完您的工作后,您需要在 onPostExecute() 中返回完成的视图。 Android 实现只允许主 UI 线程与视图交互。


t
topher217

对于 runOnUiThread() 方法的单行版本,您可以使用 lambda 函数,即:

runOnUiThread(() -> doStuff(Object, myValue));

其中 doStuff() 可以表示一些用于修改某些 UI 对象的值的方法(设置文本、更改颜色等)。

我发现这在尝试更新多个 UI 对象时更简洁,而不需要在每个 the most upvoted answer 中提到的 6 行 Runnable 定义,这绝不是不正确的,它只是占用更多空间,我发现可读性差。

所以这:

runOnUiThread(new Runnable() {
    @Override
    public void run() {
        doStuff(myTextView, "myNewText");
    }
});

可以变成这样:

runOnUiThread(() -> doStuff(myTextView, "myNewText"));

doStuff 的定义在别处。

或者如果你不需要这么通用,只需要设置一个 TextView 对象的文本:

runOnUiThread(() -> myTextView.setText("myNewText"));

N
Nalin

如果您只是想从非 UI 线程中无效(调用重绘/重绘函数),请使用 postInvalidate()

myView.postInvalidate();

这将在 UI 线程上发布一个无效请求。

欲了解更多信息:what-does-postinvalidate-do


r
rahat

好吧,你可以这样做。

https://developer.android.com/reference/android/view/View#post(java.lang.Runnable)

一个简单的方法

currentTime.post(new Runnable(){
            @Override
            public void run() {
                 currentTime.setText(time);     
            }
        }

它还提供延迟

https://developer.android.com/reference/android/view/View#postDelayed(java.lang.Runnable,%20long)


很高兴看到你发布答案!请记住,您应该添加一些有关您修复的内容或正确原因的详细信息。它可以帮助用户了解正在发生的事情,而不仅仅是为用户提供答案。将其视为您所有答案的规则。我希望你过得愉快。
a
ann

对我来说,问题是我从我的代码中显式调用了 onProgressUpdate()。不应该这样做。我改为调用 publishProgress() 并解决了错误。


P
Peter Mortensen

在我的例子中,我在 Adaptor 中有 EditText,它已经在 UI 线程中。但是,当这个 Activity 加载时,它会因为这个错误而崩溃。

我的解决方案是我需要从 XML 中的 EditText 中删除 <requestFocus />


T
Tarun Kumar

对于在 Kotlin 中苦苦挣扎的人来说,它是这样工作的:

lateinit var runnable: Runnable //global variable

 runOnUiThread { //Lambda
            runnable = Runnable {

                //do something here

                runDelayedHandler(5000)
            }
        }

        runnable.run()

 //you need to keep the handler outside the runnable body to work in kotlin
 fun runDelayedHandler(timeToWait: Long) {

        //Keep it running
        val handler = Handler()
        handler.postDelayed(runnable, timeToWait)
    }

U
Udara Kasun

如果你找不到 UIThread 你可以使用这种方式。

yourcurrentcontext 的意思是,你需要解析当前上下文

 new Thread(new Runnable() {
        public void run() {
            while (true) {
                (Activity) yourcurrentcontext).runOnUiThread(new Runnable() {
                    public void run() { 
                        Log.d("Thread Log","I am from UI Thread");
                    }
                });
                try {
                    Thread.sleep(1000);
                } catch (Exception ex) {

                }
            }
        }
    }).start();

M
Mick

如果您在一个片段内,那么您还需要获取活动对象,因为 runOnUIThread 是活动上的一个方法。

Kotlin 中的一个示例,其中包含一些周围的上下文,以使其更清晰 - 这个示例是从相机片段导航到画廊片段:

// Setup image capture listener which is triggered after photo has been taken
imageCapture.takePicture(
       outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {

           override fun onError(exc: ImageCaptureException) {
           Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
        }

        override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                        val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                        Log.d(TAG, "Photo capture succeeded: $savedUri")
               
             //Do whatever work you do when image is saved         
             
             //Now ask navigator to move to new tab - as this
             //updates UI do on the UI thread           
             activity?.runOnUiThread( {
                 Navigation.findNavController(
                        requireActivity(), R.id.fragment_container
                 ).navigate(CameraFragmentDirections
                        .actionCameraToGallery(outputDirectory.absolutePath))
              })

M
Mohamed Ben Romdhane

对于任何使用片段的人:

(context as Activity).runOnUiThread {
    //TODO
}

K
Kaushal Sachan

已解决:只需将此方法放在 doInBackround 类中...并传递消息

public void setProgressText(final String progressText){
        Handler handler = new Handler(Looper.getMainLooper()) {
            @Override
            public void handleMessage(Message msg) {
                // Any UI task, example
                progressDialog.setMessage(progressText);
            }
        };
        handler.sendEmptyMessage(1);

    }

林果皞

在我的情况下,调用者在短时间内调用太多次会得到这个错误,如果太短,我只是把经过的时间检查不做任何事情,例如,如果函数被调用少于 0.5 秒,则忽略:

    private long mLastClickTime = 0;

    public boolean foo() {
        if ( (SystemClock.elapsedRealtime() - mLastClickTime) < 500) {
            return false;
        }
        mLastClickTime = SystemClock.elapsedRealtime();

        //... do ui update
    }

更好的解决方案是禁用单击按钮并在操作完成时再次启用它。
@lsrom 在我的情况下并不是那么简单,因为调用者是内部的第 3 方库并且不受我的控制。