写点什么

Android 子线程更新 UI 了解吗?,看这一篇就够了

用户头像
Android架构
关注
发布于: 24 分钟前

那我们可不可以绕过这个 checkThread 方法呢?来达到子线程访问 UI,我们先看一段代码:


public class MainActivity extends AppCompatActivity {private TextView tvTest;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tvTest = findViewById(R.id.tvTest);new Thread(new Runnable() {@Overridepublic void run() {tvTest.setText("测试子线程加载");}}).start();}}


这段代码是可以直接运行成功的,并且没有任何问题,那这是是为什么呢?可能你已经猜想到这是为什么了—— 绕过了 checkThread 方法。


下面来分析一下原因: 访问及刷新 UI,最后都会调用到 ViewRootImpl,如果对 ViewRootImpl 还很陌生,可以参考我的另一篇博客 [Android 绘制原理浅析【干货】](


)。


那么直接在 onCreate 启动时,ViewRootImpl 肯定还没启动起来啊,不然,那刷新肯定失败,我们可以验证一下。把上面 Thread 里面加一个延迟,变成这样


public class MainActivity extends AppCompatActivity {private TextView tvTest;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);tvTest = findViewById(R.id.tvTest);new Thread(new Runnable() {@Overridepublic void run() {try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}tvTest.setText("测试子线程加载");}}).start();}}


运行起来直接崩溃


android.view.ViewRootImpl1.run(MainActivity.java:27)


和猜想一致,那么 ViewRootImpl 是什么时候被启动起来的呢? 在[Android 绘制原理浅析【干货】](


) 中提到,当 Activity 准备好后,最终会调用到 Activity 中的 makeVisible,并通过 WindowManager 添加 View,代码如下


//Activityvoid makeVisible() {if (!mWindowAdded) {ViewManager wm = getWindowManager();wm.addView(mDecor, getWindow().getAttributes());mWindowAdded = true;}mDecor.setVisibility(View.VISIBLE);}


看一下 wm addView 方法


//WindowManagerImplpublic void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {applyDefaultToken(params);mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);}


在看一下 mGlobal.addView 方法


//WindowManagerGlobalpublic void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow) {ViewRootImpl root;.....View panelParentView = null;synchronized (mLock) {root = new ViewRootImpl(view.getContext(), display);view.setLayoutParams(wparams);mViews.add(view);mRoots.add(root);}...}


终于找到了 ViewRootImpl 的创建。那么回到上面 makeVisible 是什么时候被调用到的呢? 看 Activity 启动流程时,我们知道,Ativity 的启动和 AMS 交互的代码在 ActivityThread 中,搜索 makeVisible 方法,可以看到调用地方为


//ActivityThreapublic void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,String reason) {...if (r.activity.mVisibleFromClient) {r.activity.makeVisible();}...}


private void updateVisibility(ActivityClientRecord r, boolean show) {....if (show) {if (!r.activity.mVisibleFromServer) {if (r.activity.mVisibleFromClient) {r.activity.makeVisible();}...}


//调用 updateVisibility 地方为 handleStopActivity() handleWindowVisibility() handleSendResult()


这里我们只关注 ViewRootImpl 创建的第一个地方,从 Acitivity 声明周期 handleResumeActivity 会被优先调用到,也就是说在 handleResumeActivity 启动后(OnResume),ViewRootImpl 就被创建了,这个时候,就无法在在子线程中访问 UI 了,上面子线程延迟了一会,handleResumeActivity 已经被调用了,所以发生了崩溃。

SurfaceView 是为什么可以直接子线程绘制呢?

在[Android 绘制原理浅析【干货】](


) 提到了,我们一般的 View 有一个 Surface,并且对应 SurfaceFlinger 的一块内存区域。这个本地 Surface 和 View 是绑定的,他的绘制操作,最终都会调用到 ViewRootImpl,那么这个就会被检查是否主线程了,所以只要在 ViewRootImpl 启动后,访问 UI 的所有操作都不可以在子线程中进行。


那 SurfaceView 为什么可以子线程访问他的画布呢?如下


public class MainActivity extends AppCompatActivity implements SurfaceHolder.Callback {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);SurfaceView surfaceView = findViewById(R.id.sv);surfaceView.getHolder().addCallback(this);}


@Overridepublic void surfaceCreated(final SurfaceHolder holder) {new Thread(new Runnable() {@Overridepublic void run() {while (true){Canvas canvas = holder.lockCanvas();canvas.drawColor(Color.RED);holder.unlockCanvasAndPost(canvas);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}}}).start();}@Overridepublic void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {}}


其实查看 SurfaceView 的代码,可以发现他自带一个 Surface


public class SurfaceView extends View implements ViewRootImpl.WindowStoppedCallback {...final Surface mSurface = new Surface();


...}


在 SurfaceView 的 updateSurface()中


protected void updateSurface() {....if (creating) {//View 自带 Surface 的创建 mSurfaceSession = new SurfaceSession(viewRoot.mSurface);mDeferredDestroySurfaceControl = mSurfaceControl;updateOpaqueFlag();final String name = "SurfaceView - " + viewRoot.getTitle().toString();mSurfaceControl = new SurfaceControlWithBackground(name,(mSurfaceFlags & SurfaceControl.OPAQUE) != 0,new SurfaceControl.Builder(mSurfaceSession).setSize(mSurfaceWidth, mSurfaceHeight).setFormat(mFormat).setFlags(mSurfaceFlags));}


//SurfaceView 中自带的 Surfaceif (creating) {mSurface.copyFrom(mSurfaceControl);}


....}


SurfaceView 中的 mSurface 也有在 SurfaceFlinger 对应的内存区域,这样就很容易实现子线程访问画布了。


这样设计有什么不好的地方吗?


因为这个 mSurface 不在 View 体系中,它的显示也不受 View 的属性控制,所以不能进行平移,缩放等变换,也不能放在其它 ViewGroup 中,一些 View 中的特性也无法使用。

别踩百块

我们知道 SurfaceView 可以在子线程中刷新画布(所称的离屏刷新),那做一些刷新频率高的游戏,就很适合.下面我们开始撸一个前些年比较火的小游戏。



看游戏分为几个步骤,这里主要讲一下原理和关键代码(下面有完整代码地址)


  • 绘制一帧

  • 动起来

  • 手势交互

  • 判断游戏是否结束

  • 优化内存

绘制一帧


我们把一行都成一个图像,那么他有一个黑色块,和多个白色块组成. 那就可以简单抽象为:


public class Block {private int height;private int top;private int random = 0; //第几个是黑色块}


绘制逻辑


public void draw(Canvas canvas,int random){this.random=random;canvas.save();for(int i=0;i<WhiteAndBlack.DEAFAUL_LINE_NUME;i++){if(random == i){blackRect=new Rect(left+iwidth,top,width+widthi,top+height);canvas.drawRect(left+iwidth,top,width+widthi,top+height,mPaint);}else if(error == i){canvas.drawRect(left+iwidth,top,width+widthi,top+height, errorPaint);}else{canvas.drawRect(left+iwidth,top,width+widthi,top+height,mDefaultPaint);}}canvas.restore();}


那么一行的数据有了,我只需要一个 List 就可以绘制一屏幕的数据


//List<Block> list;private void drawBg() {synchronized (list) {mCanvas.drawColor(Color.WHITE);if (list.size() == 0) {for (int i = 0; i <= DEAULT_HEIGHT_NUM; i++) {addBlock(i);}} else {......}}}


private void addBlock(int i) {Block blok = new Block(mContext);blok.setTop(mHeight - (mHeight / DEAULT_HEIGHT_NUM) * i);int random = (int) (Math.random() * DEAFAUL_LINE_NUME);blok.


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


draw(mCanvas, random);list.add(blok);}

要让其动起来

SurfaceView 在不断的刷新,那么只要让 List 里面的数据每一行的 top 不断增加,下面没有数据了,直接添加到上面


//SurfaceView 新开的子线程 Thread@Overridepublic void run() {isRunning=true;while (isRunning){draw();}}


private void draw() {try {mCanvas = mHolder.lockCanvas();if(mCanvas !=null) {drawBg();// removeNotBg();

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android 子线程更新UI了解吗?,看这一篇就够了