刚开始在 Android 里面使用多线程, 立即就碰钉了. java 正常的代码在Android 里面就有错误, 什么"Only the original thread that created a view hierarchy can touch its views" 等错误都出现了.

在 http://www.android123.com.cn/androidkaifa/386.html 算是找到答案了.

  • 这主要是Android的相关View和控件不是线程安全的. 由于View 不是线程安全的, 你起了几个线程更新同一个view, 那结果就很难保证了. 为此, android 不让我们自己建立的线程乱执行.
  • 为了保证View 的同步, 我们可以使用 Handler 类对象! 个人理解, Handler 对象就是要把我们涉及到 View 的修改操作拉会到 Main(UI) 线程中 , 只要你在新开的线程类的run 方法中调用 mHandler.sendEmptyMessage(msg); 或 mHandler.post(runnable), 那 Handle 对象就能把你的操作拉回到 Main 线程中;
  • 可能你会说, 我在线程类的run 方法中不用mHandler.sendEmptyMessage(msg); 或 mHandler.post(runnable)方法. 如果你有对View 的操作, 运行一下, 恭喜你!! 抛出异常了.
  • 所以, 当我们有耗时间的操作, 并且这些操作涉及到 View, 那我们就必需把取数据以及分析数据的操作和对 View 的操作分开. 把取数据以及分析数据的耗时间操作放到一个新线程中执行, 没有涉及 View 更新操作的 Thread 对象, 随便你 start(). 涉及 View 更新操作在 Handle 对象中进行.
  • 以前没用过 Handle, 以上说法可能很肤浅, 大家见谅!
  • Html 在页面加载完成后, 有个 onLoad() 的回调方法. 貌似Activity 没有类似的回调方法呢?

Timer timer = new Timer();
timer.schedule(new MyTask(0), 500); //MyTask 继承自 TimerTask, 0.5秒后执行

private class MyTask extends TimerTask{
private int msg = 0;
public MyTask(){
super();
}
public MyTask(int msg){
this.msg = msg;
}
@Override
public void run() {
mHandler.sendEmptyMessage(msg);//用了handler, 所以这个任务只会在UI 层加载完成后在Main 线程中执行.
}
}

一般在刚开始开发android时,会犯一个错误,即在View的构造函数中获取getWidth()和getHeight(),当一个view对象创建时,android并不知道其大小,所以getWidth()和getHeight()返回的结果是0,真正大小是在计算布局时才会计算,所以会发现一个有趣的事,即在onDraw( ) 却能取得长宽的原因。(这段话是转载的)

  • 今天取一个动态的TextView 的高度getHeight(), 结果一直是零. 郁闷了挺久, 终于知道了答案. 这里我又有一个疑问了, 这个TextView 的布局是在什么时候呢?

据我现在所知道的是:

  1. 在onCreate 方法中调用 setContentView(R.layout.main); 和 动态添加的TextView对象等, getHeight() 都为0!
  2. Main线程的信息队列 中, 通过 Handle 对象的handleMessage() 方法对 main.xml 的对象和前面动态添加的 TextView对象等进行 getHeight() 不为0! 这样可以清楚一点了, 那就是Activity 的初始化页面布局, 在我们的信息被信息处理之前.
  3. 如果我们的信息中又动态添加了 TextView对象呢? 经过本人的测试 getHeight() 是为0的! 但是在下一个信息中, 我们可以正确获取该 TextView 对象的高度了, 证明该对象已经布局好了. 该 TextView 对象的布局是在对应信息处理后 .
  4. 在<深入理解Android 信息处理系统> 中, 有意外的发现, 原来Activity 的页面初始化, 也是通过信息的方式进行的, 并且其信息是最早加入到信息队列中的, 理所当然Activity 的页面初始化的信息也是最早被处理的.

这样的话, 要获取UI 元素的布局信息, 如宽度和高度等, 必须在动态创建该元素的信息被处理完毕.

随便9 中的scrollTo 问题也算有个答案了.

  • 为什么在 onDraw() 中却能取得长宽(没有测试过, 下面纯属猜测)

至于为什么在 onDraw() 中却能取得长宽, 是因为onDraw(Canvas canvas) 在 View 的draw(Canvas canvas) 方法被调用了. draw() 方法把View 对象(包括它的子View 对象)都画到了 Canvas 对象里面(双缓冲中的一个缓冲, 缓冲到内存里面), 画的过程就是一个布局的过程, 当然这个时候布局还没更新到屏幕中.

View.java 源代码中把整个过程分为6 步骤, 如下:

/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

而我们的onDraw() 方法是在第三步被调用了. 在draw() 方法被调用前, mBottom, mTop 等貌似已经赋值了, height = mBottom - mTop.

/**
* The distance in pixels from the top edge of this view's parent
* to the top edge of this view.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "layout")
protected int mTop;
/**
* The distance in pixels from the top edge of this view's parent
* to the bottom edge of this view.
* {@hide}
*/
@ViewDebug.ExportedProperty(category = "layout")
protected int mBottom;

java 元注释? View 对象怎么做到给 mBottom 赋值, 我也不清楚呢. 有空再看看 ViewDebug.java 的源代码吧.

前面我们已经知道了, View 的更新都是在 UI Main 线程中进行的, 有没有例外的呢? 答案是确定的!!!!

  • SurfaceView, 这是一个可以"异步" 并且"局部" 更新的 View, 适用于动作游戏和频繁更新的页面.
  1. 异步, 更新 SurFaceView 的显示内容, 我们可以开多一个线程来进行, 这个线程通过 SurfaceView对象的变量 SurfaceHolder对象, 获取一个Canvas 对象, 在该Canvas 对象上画图, 最后把 Canvas 的内容更新到屏幕上(标准的双缓冲). 这个过程是不必在UI Main 线程中进行的.
  2. 局部, 上面的Canvas(画布) 对象, 默认是获取整个SurfaceView 的视图大小的画布, 但是有时候, 我们可能更新的地方只有某个角落. 我们可以在获取 Canvas 对象的时候, 通过 Rect 对象参数来指定一个矩形区域, 在本次的更新中, 将只更新这个区域, 其他区域将不会被更新到, 从而提高效率.
  3. 在头两个画面中, 由于Canvas对象都为空, 所以这两个画面不存在局部更新, 都是全屏更新 . 为什么头两个画面的 Canvas对象为空, 请看下面的内容.

在官方文档中, "Dev Guide" -> 左边菜单点击 "Graphics", 在出来的文档中, 最后有一段话值得注意:

Draw with a Canvas

On a SurfaceView

Note: On each pass you retrieve the Canvas from the SurfaceHolder, the previous state of the Canvas will be retained. In order to properly animate your graphics, you must re-paint the entire surface. For example, you can clear the previous state of the Canvas by filling in a color with drawColor() or setting a background image with drawBitmap() . Otherwise, you will see traces of the drawings you previously performed.

1 2 3 4 => 更新的帧数

A B => 1,2帧 更新Canvas, 后面的帧不再画东西

A B => 保留的前一个Canvas状态

A B A B => 显示内容, 只考虑更新区域(局部或全屏).

  • 在第一帧的时候, 获取的 canvas为空, 画上东西A, 画布状态 canvasA, 其前一个状态为空.
  • 到第二帧的时候, 获取的 canvas 为canvasA 的前一个状态(- -, 让人看不懂)为空, 画上东西B, 画布状态 canvasB, 其前一个状态为 canvasA.
  • 到第三帧的时候, 获取的 canvas 为canvasB 的前一个状态 canvasA, 没画东西, 画布状态 canvasA, 其前一个状态为 canvasB.

如果有画东西C, 如果画的地方没有drawColor() 或 drawBitmap(), 那么画布状态为 canvasAC, 注意是重叠 , 而非覆盖, 所以在进行动画的区域, 请先清屏了再画, 不然将有前两帧的画面痕迹.

drawColor() 是全屏清屏, drawBitmap() 可以自己控制范围, 该方法的重载方法很多.

  • 到第四帧的时候, 获取的 canvas 为canvasA 的前一个状态 canvasB, 没画东西, 画布状态 canvasB, 其前一个状态为 canvasA.

如果第三帧有画东西C, 并且这帧也画了东西D, 那画布状态为 canvasBD, 其前一个状态为 canvasAC.

  • 照此循环下去, 结果就是两个画面不停的交替, 本人测试的结果确实是这样.

在N-2>0的情况下, 第N 帧获取的画布竟然是 N-2 帧的画布, 而不是延续上一帧的画布, 并且画布被锁定了, 想换都不行.

本人菜鸟, 不是很理解android 这样做的用意. 让画面交替显示, 明显大部分情况下都不需要这样做吧, 就算需要交替画面, 我们也可以自己控制的. 并且第二帧过后, 我们每个SurfaceView 都将拥有2 个canvas 资源, 难道canvas 资源不值钱!!!!!

难道是 N-1 帧的canvas 在"页面显示后" 至 "新页面显示前" 这个过程中被系统或其他资源 "绑架" 了, 不给其他人动用, 本人无限 YY 中!!!!!

  • 如果动画只在局部更新, 为了让局部更新区域外的区域能显示同一个画面, 我们需要在第一和第二帧连续画同样的东西.

画 AB AC D E F G =>A画在不更新的区域, BCD...G 画在更新区域

显示 AB AC AD AE AF AG

  • 上面的情况, 如果第一和第二帧还没画 A ,但是在后面要显示A, 怎么办呢? 其实很简单, 在需要显示A 的时候, 把动画变为全屏更新, 连续画 A 两次后, 再把动画切换为局部更新模式.

局部更新 ->全屏更新->局部更新

画 B C D E AF AG H I J

显示 B C D E AF AG AH AI AJ =>设定A占有的区域比其他的多

  • 如果动画是全屏更新, 那只要我们连续两帧画同样的东西, 那画面就能一直显示同一个画面.

画 A B C C D E F

显示 A B C C CD CE CF =>设定C占有的区域比其他的多.

ps. 获取全屏更新的画布---------->canvas = Holder.lockCanvas();

获取局部更新的画布----------->canvas = Holder.lockCanvas(new Rect(30,30,160,160)); //范围Rect对象自己定义.

Holder 为SurfaceView 的SurfaceHolder 对象变量.

双缓冲技术, 双即二! 一个缓冲是什么? 另一个缓冲又是什么? 本人一直的理解是, 一个缓冲是内存(甚至是CPU的缓存), 一个是显示设备.

画图的耗时和显示的耗时相比, 前者很多情况下明显要大于后者, 如果画图的操作过于复杂, 那这个差距就更大了. 一边画, 一边更新到屏幕上, 由于更新的频率快并且不均匀, 将造成一个后果--闪烁.

对于闪烁问题, 我们的前辈很聪明, 给出了一个双缓冲的方法. 很多情况下是, 我们先把显示内容画在内存中的一张位图上, 画好了, 再把这张位图拷贝到屏幕上. 这样做的好处是, 位图拷贝到屏幕上的耗时是比较稳定的, 在合理频率下, 画面的显示就会很稳定. 至于画图的操作有多复杂多困难, 跟画面显示没直接的关系了, 画面显示只能内存打交道, 画图也是在内存中暗地里进行着.

以上是个人的理解, 没有参考什么权威说法之类的东东, 反正我不认同那些说 SurfaceView 因为用了两个canvas, 所以是双缓冲的说法.

现在比较新的UI 框架, 有哪个的内置控件(组件)不是双缓冲的呢? 一般人能想到的, 那些大公司的高手没可能想不到, 控件都闪烁, 还不给人笑死.

更多相关文章

  1. android 线程间通信
  2. Android 多线程注意事项
  3. Android 学习笔记 Thread (一) android线程
  4. Android 实现联网——在线程中联网
  5. Android 随时随地键值对存储对象解决方案
  6. android对象池之Message
  7. Android的绘制文本对象FontMetrics的介绍及绘制文本
  8. android UI线程安全问题

随机推荐

  1. Android make脚本简记
  2. Android(安卓)UI:筛选条的简单实现
  3. Android错误解决 Call requires API leve
  4. android登录tomcat服务器并查找数据库的
  5. android飞机游戏敌机移动路径
  6. Android最基本的异步网络请求框架
  7. Android(安卓)KitCat 4.4.2 ADB 官方所支
  8. Android系统添加自己写的工具
  9. Android中的Ninja简介
  10. android 项目R文件丢失解决办法