刚开始在 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 的布局是在什么时候呢?
据我现在所知道的是:
- 在onCreate 方法中调用 setContentView(R.layout.main); 和 动态添加的TextView对象等, getHeight() 都为0!
- 在Main线程的信息队列 中, 通过 Handle 对象的handleMessage() 方法对 main.xml 的对象和前面动态添加的 TextView对象等进行 getHeight() 不为0! 这样可以清楚一点了, 那就是Activity 的初始化页面布局, 在我们的信息被信息处理之前.
- 如果我们的信息中又动态添加了 TextView对象呢? 经过本人的测试 getHeight() 是为0的! 但是在下一个信息中, 我们可以正确获取该 TextView 对象的高度了, 证明该对象已经布局好了. 该 TextView 对象的布局是在对应信息处理后 .
- 在<深入理解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, 适用于动作游戏和频繁更新的页面.
- 异步, 更新 SurFaceView 的显示内容, 我们可以开多一个线程来进行, 这个线程通过 SurfaceView对象的变量 SurfaceHolder对象, 获取一个Canvas 对象, 在该Canvas 对象上画图, 最后把 Canvas 的内容更新到屏幕上(标准的双缓冲). 这个过程是不必在UI Main 线程中进行的.
- 局部, 上面的Canvas(画布) 对象, 默认是获取整个SurfaceView 的视图大小的画布, 但是有时候, 我们可能更新的地方只有某个角落. 我们可以在获取 Canvas 对象的时候, 通过 Rect 对象参数来指定一个矩形区域, 在本次的更新中, 将只更新这个区域, 其他区域将不会被更新到, 从而提高效率.
- 在头两个画面中, 由于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
or setting a background image with drawColor()
. Otherwise, you will see traces of the drawings you previously performed.drawBitmap()
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 框架, 有哪个的内置控件(组件)不是双缓冲的呢? 一般人能想到的, 那些大公司的高手没可能想不到, 控件都闪烁, 还不给人笑死.