Android编程:如何在子线程中更新TextView控件

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android编程:如何在子线程中更新TextView控件相关的知识,希望对你有一定的参考价值。

我在子线程中写了一个Socket用于getInputStream,但是怎么样把获得的内容写到UI线程的TextView控件上?求代码,谢谢

  步骤:
  1、创建Handler对象(此处创建于主线程中便于更新UI)。
  2、构建Runnable对象,在Runnable中更新界面。
  3、在子线程的run方法中向UI线程post,runnable对象来更新UI。
  代码
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.SystemClock;
  
  import android.app.Activity;
  import android.content.Intent;
  import android.view.Menu;
  import android.view.SurfaceHolder;
  import android.view.View;
  import android.widget.Button;
  import android.widget.TextView;
  public class MainActivity extends Activity
  private Button button;
  private TextView textview;
  private final int SPLASH_DISPLAY_LENGHT = 1;
  private static int flag = 0;
  private static int count=0;
  private int ab=1;
  private boolean isrun = true;
  @Override
  protected void onCreate(Bundle savedInstanceState)
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  button = (Button)findViewById(R.id.button1);
  textview = (TextView)findViewById(R.id.textView1);
  final Handler handler= new Handler();
  
  final Runnable runnable = new Runnable()
  public void run()
  textview.setText(Integer.toString(ab));
  
  ;
  

  
  final Thread t = new Thread()
  //public boolean isrun=true;
  @Override
  public void run()
  while(isrun)
  
  handler.post(runnable); //加入到消息队列  这样没有启动新的线程,虽然没有报异常。但仍然阻塞ProgressDialog的显示
  ab++;
  try
  sleep(1000); //直接调用
   catch (InterruptedException e)
  return;
  
  
  
  ;
  t.start();
  button.setOnClickListener(new Button.OnClickListener()
  
  @Override
  public void onClick(View view)
  
  isrun=false;
  
  );
  
  
  
  
  
  
  
参考技术A 使用handler:
例:
class yourActivity extends Activity

private TextView mTextView ; //你的textView
........

........
new Thread()

@Override
public void run()

.....你的子线程处理逻辑
//下面new一个Message用来装你要传到UI线程的东西
Message msg = new Message();
msg.what = ;
msg.arg1 = ;
msg.obj = ;
//mHandler 见下面定义
mHandler.sendMessage(msg);




......
//定义一个Handler
Handler mHandler = new Handler()

@Override

public void handleMessage(Message msg)
//这里就可以从msg取出你从线程中传到UI线程的东西;
//例如
String txt = (String)msg.obj;
mTextView.setText(txt);






思路是这样了,Handler具体用法,你继续百度吧本回答被提问者采纳
参考技术B TextView tv=xxx;
//not in UI thread
tv.post(new Runable()
@Override
public void run()
//update ui widgets here
tv.setText("xxxx");

);

如何做到在子线程更新 UI?

一般来讲,子线程是不能更新 UI 的,如果在子线程更新 UI,会报错。

但在某种情况下直接开启线程更新 UI 是不会报错的。

比如,在 onCreate 方法中,直接开启子线程更新 UI,这样是不会报错的。

override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    textView = findViewById(R.id.tv)
    thread 
        textView.text = "哈哈哈哈"
    

如果在子线程中假如延时,比如加一行Thread.sleep(2000)就会报错。

这是为什么呢?

有人会说,因为睡眠了 2 s,因此 UI 的线程检查机制就已经建立了,所以在子线程更新就会报错。

更新 UI 的线程检测是什么时候开始的

子线程更新的错误定位是 ViewRootImpl 中的 checkThread 方法和 requestLayout 方法。

// ViewRootImpl 下 checkThread 的源码
void checkThread() 
    if (mThread != Thread.currentThread()) 
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    


//ViewRootImpl 下 requestLayout 的源码
@Override
public void requestLayout() 
    if (!mHandlingLayoutInLayoutRequest) 
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    

从源码中可以看出,checkThread 就是进行线程检测的方法,而调用是在 requestLayout 方法中。

要想知道 requestLayout 是何时调用的,就要知道 ViewRootImpl 是如何创建的?

因为在 onCreate 中创建子线程访问 UI,是不报错的,这也说明在 onCreate中,ViewRootImpl 还未创建。

ViewRootImpl 是何时创建的。

在 ActivityThread 的 handleResumeActivity 中调用了 performResumeActivity 进行 onResume 的回调。

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) 
    // 代码省略...

    // performResumeActivity 最终会调用 Activity 的 onResume方法
    // 调用链如下: 会调用 r.activity.performResume。
    // performResumeActivity -> r.activity.performResume -> Instrumentation.callActivityOnResume(this) -> activity.onResume();
    final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);

    // 代码省略...

    if (r.window == null && !a.mFinished && willBeVisible) 
        r.activity.mVisibleFromServer = true;
        mNumVisibleActivities++;
        if (r.activity.mVisibleFromClient) 
            // 注意这句,让 activity 显示,并且会最终创建 ViewRootImpl
            r.activity.makeVisible();
        
    

进一步跟进 activity.makeVisible()

void makeVisible() 
    if (!mWindowAdded) 
        ViewManager wm = getWindowManager();
        // 往 WindowManager 中添加 DecorView
        wm.addView(mDecor, getWindow().getAttributes());
        mWindowAdded = true;
    
    mDecor.setVisibility(View.VISIBLE);

WindowManager 是一个接口,它的实现类是 WindowManagerImpl

// WindowManagerImpl 的 addView 方法
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) 
    applyDefaultToken(params);
    // 最终调用了 WindowManagerGlobal 的 addView 
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);


// WindowManagerGlobal 的 addView
public void addView(View view, ViewGroup.LayoutParams params,
                    Display display, Window parentWindow) 
    // 省略部分代码

    // ViewRootImpl 对象的声明
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) 
        // 省略部分代码

        // ViewRootImpl 对象的创建
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        try 
            // 调用 ViewRootImpl 的 setView 方法
            root.setView(view, wparams, panelParentView);
         catch (RuntimeException e) 
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) 
                removeViewLocked(index, true);
            
            throw e;
        
    

由此可以看出,ViewRootImpl 是在 activity 的 onResume 方法调用后才由 WindowManagerGlobal 的 addView 方法创建。

那 requestLayout 是如何调用的呢?

在上面 WindowManagerGlobal 的 addView 方法中,创建完 ViewRootImpl 后,会调用它的 setView 的方法,在 setView 方法内部会调用 requestLayout

此时就会去检测 UI 更新时调用的线程了。

// ViewRootImpl 的 setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) 
    synchronized (this) 
        if (mView == null) 
            mView = view;

            // 省略无关代码...
            // requestLayout 的调用
            requestLayout();

          // 省略无关代码...
        


// requestLayout 方法
@Override
public void requestLayout() 
    if (!mHandlingLayoutInLayoutRequest) 
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    

而在 SheduleTranversals 方法中,会调用 TraversalRunnable 的 run方法,最终会在 performTraversals 方法中,调用 performMeasure performLayout performDraw 去开始 View 的绘制流程。

void scheduleTraversals() 
    if (!mTraversalScheduled) 
        mTraversalScheduled = true;
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // TraversalRunnable 的 run 方法中,会开启 UI 的measure、layout、draw
        mChoreographer.postCallback(
            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
       // 省略无关代码...
    


final class TraversalRunnable implements Runnable 
    @Override
    public void run() 
        doTraversal();
    

void doTraversal() 
    if (mTraversalScheduled) 
        // 省略部分代码
        performTraversals();
    


private void performTraversals() 
    // Ask host how big it wants to be
    // 省略部分代码
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    performLayout(lp, mWidth, mHeight);
    performDraw();

子线程更新 UI 实战

既然知道了子线程更新 UI 的检测是在 checkThread 方法中,那么有没有什么方法可以绕过呢?能否做到子线程更新 UI 呢?

答案是可以的。

我以一个简单的 demo 实验一下,下面先看效果。

代码如下:

// MainActivity
public class MainActivity extends AppCompatActivity 
    private View containerView;
    private ViewTreeObserver.OnGlobalLayoutListener globalLayoutListener;
    private TextView mTv2;
    private TextView mTv1;

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

        containerView = findViewById(R.id.container_layout);
        mTv1 = findViewById(R.id.text);
        mTv2 = findViewById(R.id.text2);

        // 开启线程,启动 GlobalLayoutListener
        Executors.newSingleThreadExecutor().execute(() -> initGlobalLayoutListener());
    

    private void initGlobalLayoutListener() 
        globalLayoutListener = () -> 
            Log.e("caihua", "onGlobalLayout : " + Thread.currentThread().getName());
            ViewGroup.LayoutParams layoutParams = containerView.getLayoutParams();
            containerView.setLayoutParams(layoutParams);
        ;
        this.getWindow().getDecorView().getViewTreeObserver().addOnGlobalLayoutListener(globalLayoutListener);
    


    public void updateUiInMain(View view) 
        mTv1.setText("主线程更新 UI");
    

    public void updateUiInThread(View view) 
        new Thread()
            @Override
            public void run() 
                SystemClock.sleep(2000);
                mTv2.setText("子线程更新 UI :" + Thread.currentThread().getName());
            
        .start();
    


原理:通过 ViewTreeObserver.OnGlobalLayoutListener 设置全局的布局监听,然后在 onGlobalLayout 方法中,调用 view 的 setLayoutParams 方法,setLayoutParams 方法内部会调用 requestLayout,这样就可以绕过线程检测。

为什么能绕过呢?

因为 setLayoutParams 中调用的 requestLayout 方法并不是 ViewRootImpl 中 requestLayout.

而 View 的 requestLayout 并不调用 checkThread 方法去检测线程。

源码如下↓

// view.setLayoutParams 源码
public void setLayoutParams(ViewGroup.LayoutParams params) 
    if (params == null) 
        throw new NullPointerException("Layout parameters cannot be null");
    
    mLayoutParams = params;
    resolveLayoutParams();
    if (mParent instanceof ViewGroup) 
        ((ViewGroup) mParent).onSetLayoutParams(this, params);
    
    // 调用 requestLayout 方法。
    requestLayout();

// View 的 requestLayout 方法
public void requestLayout() 
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) 
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null && viewRoot.isInLayout()) 
            if (!viewRoot.requestLayoutDuringLayout(this)) 
                return;
            
        
        mAttachInfo.mViewRequestingLayout = this;
    

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null && !mParent.isLayoutRequested()) 
        mParent.requestLayout();
    
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) 
        mAttachInfo.mViewRequestingLayout = null;
    

以上是关于Android编程:如何在子线程中更新TextView控件的主要内容,如果未能解决你的问题,请参考以下文章

Android 在子线程中更新UI的几种方法示例

Android--Handler的用法:在子线程中更新界面

android编程为啥要更新界面?怎么更新?

Android第一行代码--Service(四大组件之一)

Android第一行代码--Service(四大组件之一)

Android第一行代码--Service(四大组件之一)