• 关于Android开发实在是有太多太多东西可以写了(很多在网上都找不到),但是又没心情在工作中途停下来详细写那么多东西,所以在这里简单、凌乱地写下目前想到的一些地方(不断更新,想到就加),等完工后有空写个Effective Android系列来帮助程序猿们少走弯路。

    (1)JAVA的static初始化次序是按照语句顺序,在使用单件模式时特别要注意这点。

    (2)DrawingCache机制的剖析和正确使用。

    (3)Animation机制的剖析和各种改造与hook。 以及如何让animation最流畅(该问题的细节估计比大多数人知道的要复杂)。

    (4)软键盘隐藏\显示的完美控制。

    (5)EditText的擦除手势+点击对软键盘隐藏\显示的控制

    (6)ListView全面向下兼容的scrollBy效果(android 2.3及才开始提供的动画滚动指定距离的效果)

    (7)程序向下兼容的设计

    (8)单点缩放手势

    (9)WP7多种独家动画效果和控件的山寨指南

    (10)一个基于skia的矢量图系统

    (11)一个高效的Bitmap管理类

    (12)边缘手势设计

    (13)一个完美的AutoScrollTextView(过长的文字会来回移动,否则保持TextView的观感)

    (14)一个倒置的ListView实现(即ListView是按Bottom对齐,这个可能懒得写,因为最终没用上)

    (15)android消息循环、布局机制带来的各种注意事项

    (16)如何完美退出App(既保持Activity进行finish时的流畅切换动画,又保证所有资源被彻底释放)

    (17)iOS级平滑的滑动效果实现(包括惯性、自动归位)

    (待续……)

  • 1. 在2.1以下版本,如果对某ViewGroup进行setDrawingCacheEnabled(true),则该ViewGroup中任何child的动画都将无法显示(或者说只刷新了第一帧,然后就不刷新了)。在2.1及以上版本无此问题

    原因:对比2.0和2.1版本的View源码,可以发现在buildDrawingCache这一方法的最后一段有改动:

    2.0源码:

                mPrivateFlags |= DRAWN;

                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
                    if (ViewDebug.TRACE_HIERARCHY) {
                        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
                    }
                    mPrivateFlags &= ~DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }

                canvas.restoreToCount(restoreCount);

                if (attachInfo != null) {
                    // Restore the cached Canvas for our siblings
                    attachInfo.mCanvas = canvas;
                }
                mPrivateFlags |= DRAWING_CACHE_VALID;

    2.1源码:

                mPrivateFlags |= DRAWN;
                mPrivateFlags |= DRAWING_CACHE_VALID;

                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) {
                    if (ViewDebug.TRACE_HIERARCHY) {
                        ViewDebug.trace(this, ViewDebug.HierarchyTraceType.DRAW);
                    }
                    mPrivateFlags &= ~DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }

                canvas.restoreToCount(restoreCount);

                if (attachInfo != null) {
                    // Restore the cached Canvas for our siblings
                    attachInfo.mCanvas = canvas;
                }

    也就是mPrivateFlags |= DRAWING_CACHE_VALID;这一句位置的错误导致了2.1以下的bug。为什么呢,你只要debug跟踪一下便会明白,在dispatchDraw这一方法中,特别有一段代码来处理child要进行animation的情况。当child要进行animation,则会把parent进行invalidate(),使parent会不断刷新。而2.0的代码中,parent的invalidate虽然执行了,但其效果又被最后一句mPrivateFlags |= DRAWING_CACHE_VALID给抹去了,故而产生了上述的bug情况。

  • androidDrawing Cache机制是文档中除了函数说明就几乎没提过的东西,网上各种文章和论坛讨论也是语焉不详、胡乱猜测。而这一机制却决定着android是否能真正打造高级用户体验。花了一整天研究源码后,下面我将讨论这个问题,对界面效率并不执着者请绕行。

     

     

    1Drawing Cache的作用

    答:每个ViewGroupDispatchDraw它的child时,会先getDrawingCache(位图)。如果DrawingCache存在,则直接使用cache来绘制,反之才调用childdraw方法。这样就牺牲了一定的内存,但减少了draw方法的开销。对于复杂的View,这一代价是相当值得的。

     

     

    2、该使用Drawing Cache的哪些API

    答:实际只有两个API是我推荐使用的:setDrawingCacheEnabledsetDrawingCacheBackgroundColor。其实有很多方法来操纵甚至自己实现Drawing Cache机制,但这里节省时间,我只谈论个人认为的最佳实践。

    setDrawingCacheEnabled是用于打开或关闭自动的cahce机制。这个自动是啥意思呢?就是绘制时总是会取得cache,如果cache不存在或者invalid了,那么就更新生成cache

    setDrawingCacheBackgroundColor在我们手中的用途是设置一个背景颜色缓存(必须是非0值),从而可以使用RGB_565的格式来生成cache,省略透明通道从而减少cache占用的空间和运算量。这一函数不一定符合你的需求,但在能够调用的时候尽量记得去调用。

     

    我推荐的做法如下:

    (1)对于每一个不是经常变化的view,使用setDrawingCacheEnabled(true),并尽量使用setDrawingCacheBackgroundColor。什么是经常变化的View呢?EditText因为光标闪烁就是个例子。任何经常定时调用invalid方法的view都不要去cache

    (2)要注意避免多余的cache,比如说一个ViewGroup中包含多个比较静态的child,那么你要么打开ViewGroup的cache,要么一一打开child的cache,将两者都打开是多余的。同时你要理解这两种做法的优劣区别:打开parent的cache,可以进一步减少parent绘制的代价(否则每次都要把child的cache重新绘制到一起并进行重叠计算);打开child的cache,可以在单个child发生改变时以更小的代价重建cache。但是最重要的区别在于对ViewGroup使用animation时,animation开始时必然要重建其绑定View的cache(会调用invalidate设置~DRAWING_CACHE_VALID标志位),这时候如果ViewGroup打开了cache则需要全部重建,如果child打开了cache那么只是parent额外进行一次绘制而已(每个child的cache不变)。因此,第二种做法是经常更加推荐的。

    (3)对于setDrawingCacheEnabled(true)view,在进行连续变化时,手动在变化开始前设置setDrawingCacheEnabled(false),在变化结束后重新设回true

    (4)尽量为每一个会调用AnimationViewGroup(无论它平时是否进行cache)使用setDrawingCacheBackgroundColor。这会大大减少Animation时的cache开销。

     

    按理说,依照上面四条法则,你就能最大限度地发挥Drawing cache的作用并抑制其副作用。但是,鉴于Android杯具的设计,如果你的工作中同时大量涉及AnimationDrawing Cache,你就必须有一些更透彻的认识才能避免不知不觉的错误。

     

     

    3Animationdrawing cache方面到底做了啥?

     

    答:其实是ViewGroup为了保证Animation的流畅,设计了一个Animation Cache机制。这一机制就是将ViewGroup的所有child都强制建立Drawing Cache,在Animation结束后再释放cache。其中onAnimationStart方法负责建立cacheonAnimationEnd方法负责释放cache

     

     

    4、我们一般可能犯什么错?

    答:看你是否会这样做:对于某个动画后要消失的View,使用AnimationListeneronAnimationEnd事件,在里面把要消失的Viewparentremove掉或者setVisibility(GONE)

    这样做的结果是,Animation结束时可能会卡一下,哪怕你同时clearAnimation后依然如此。

     

    为什么呢?

    因为无论你是removechild还是setVisibility(GONE),都会立刻触发destroyDrawingCache方法,也就是cache会被清除掉。

    然后再从Trace Stack看一下onAnimationEnd的触发时机,是在draw方法中,是在即将绘制View之前(就是说无论你remove了还是GONE了,都阻止不了View的绘制)。于是,当要画Animation的最后一帧时,必须重新建立cache(前面被你destroy掉了),哪怕这一帧很有可能完全不用画(比如View已经移动到屏幕外了)。

     

    如何解决?

    貌似继承View.onAnimationEnd在里面清除掉无用的View可以避免重建cache,但是我没试,因为View.onAnimationEnd本身会引起另外一个问题(后面再说)。而除此之外我根本没看到android提供了其它接口可以解决上述问题,于是不得不发明了一个trick

    trick是这样:定义一个EmptyAnimation类,除了extends Animation外什么都不做。当你对A中的child B进行Animation,结束时想把BAremove掉或者GONE掉,你应该在BAnimation开始的同时,也同时启动A的一个EmptyAnimationDurationBAnimation一样),然后在这个EmptyAnimationonAnimationEnd事件时,去执行将B移除的操作,并且记得对B进行clearAnimation

    实际上,如果你想让B在动画结束后消失,那么动画的最后一帧是不用显示的(最后一帧表达的就是:消失)。使用以上trick,就可以在B绘制最后一帧前,把B给清除掉,同时把动画也给关掉(避免调用View.onAnimationEnd),因此目前所知是最高效的办法。

     

     

    5、更蛋疼的问题

    为什么非要避开View.onAnimationEnd呢?因为ViewGroup.onAnimationEnd会将childDrawing Cache机制统统关掉,完全不管这个child原来是否setDrawingCacheEnabled(true)

    所以非常悲催,对于任何Animation,你都必须记得在AnimationListener.onAnimationEnd的时候去clearAnimation,并且手动将一些你觉得不需要的cache给关掉(因为View.onAnimationStart将child的cache都打开了)。

  • 1、往res/drawable或color文件夹添加东西后(比如一个定义ColorStateList或StateListDrawable的xml),记得把工程rebuild,否则容易出现id被添加但文件本身却未被编译的奇怪问题(会出现FileNotFoundException)。

    2、StateListDrawable的mutate方法有bug,其源代码(1.6)如下:

        @Override
        public Drawable mutate() {
            if (!mMutated && super.mutate() == this) {
                final int[][] sets = mStateListState.mStateSets;
                final int count = sets.length;
                mStateListState.mStateSets = new int[count][];
                for (int i = 0; i < count; i++) {
                    mStateListState.mStateSets[i] = sets[i].clone();
                }
                mMutated = true;
            }
            return this;
        }

        可见mStateListState.mStateSets[i] = sets[i].clone();这句并未考虑sets[i]为null的情况。而大部分时候,你只会声明某些常用状态(比如state_pressed),未声明的状态皆为null,因此该mutate方法会出错。

    3、如何自定义CheckBox

        StateList中的state_checked这个状态是如何被一个View使用上的?带着这个问题粗略看了下CompoundButton的源码后,发现这涉及到对view的internal state机制的完整理解。目前先把这个理解放到一边,单纯讨论下我希望对自己的View在StateList定义中用上这个state_check该如何做:

        方法一:继承CompoundButton这个抽象类,于是你就得到了一个具备state_checked这一状态的自定义View了。

        方法二:直接使用CheckBox这个类。CheckBox的源码说明,其仅仅是一个CompoundButton+一个CheckBox的Style定义:

      name="Widget.CompoundButton.CheckBox">
       
    <item name="android:background">@android:drawable/btn_check_label_backgrounditem>
       
    <item name="android:button">@android:drawable/btn_checkitem>

        因此你使用时用自己定义的style替换之,或者直接把background和button属性给修改掉,就能不受内置style的影响了。

     

    4. 关于在父子控件之间传递State状态的两个方法:

    父获得子State:setAddStatesFromChildren

    子获得父State:setDuplicateParentStateEnabled

    如果这两个同时被设置了,那么你将得到异常。

     

    5. XML中用的那些常量到底被android放在哪?

    放在android.R.attrs里,于是放弃XML吧,一律用code定义之。

  • 1、所有的Layout类均不向外暴露SizeChanged的事件,在控件的Size是异步计算的android里,这种做法完全无法理解(暴露一下又不是什么麻烦事),于是你只能自己再给Layout包装一层并进行暴露……真怀念WPF。

    2、某些控件的可扩展性设计非常糟糕。比如ListView没有Orientation选项,比如Gallery的选中项只能居中显示……这种例子不胜枚举。

    另外据说Eclipse ADT插件里那个界面编辑器Google开源贡献给Eclipse社区了,可我必须说那个东西实在是太烂太简陋了,我挺难想象谁真的用那个东西做开发,凡是体验过微软为.NET做的各种UI编辑器(从WinForm开始)的相信都会赞成……总而言之,Google要好好向微软学习做工具。

  • 2011.2.14

    对BitmapDrawable有了更深的认识后才发现有点错怪了Google那帮家伙,其实他们也早就意识到“原文”所提到的问题了,虽然做出补救是在SDK1.5才出现的,那就是BitmapDrawable.mutate()这个方法。

    详细可看这篇blog:http://android-developers.blogspot.com/2009/05/drawable-mutations.html

    原来Resources缓存的并不是个Drawable,而是Drawable中的State(比如BitmapDrawable的BitmapState)。是这个State起了一个share的作用,导致不同的Drawable产生了关联。而mutate就是重新创建一个State以避免共享。所以当要修改alpha时,只要mutate()后再修改即可。

    而如果要在SDK 1.5以下的版本回避这个问题,则可以采用下面原文所提的方法。

    然而,这个问题目前对于我来说已经很小儿科了,又出现了更头疼的问题,希望我能解决并分享解决方案。

     

    ===========================以下是原文=============================

    阅读之前要建立一个信念:Android就是一个给工程师折腾的东西(外观和用户体验也是),文档什么的只是浮云,更多的时候你只能去源码里折腾,否则你根本不知道Google那帮家伙到底写了啥逻辑。

    问题1:我用ImageView A做了个按钮,(用id引用drawable中的图片a),并做了个半透明效果(通过setAlpha),当用户Touch这个A时才不透明。现在我在另一处地方也需要用到图片a(不需要半透明行为),也是建立了一个新ImageView B并用id引用。然后你看看最终显示效果,会发现A和B保持着同样的外观,也就是要么A跟B一样不透明,要么B跟A一样半透明。

    解答:首先不说别的,单纯吐槽一下,两个独立的ImageView仅仅因为引用了同样一张图片,就产生了关联,设计API和背后的机制不是这么玩的啊……好吧,为了性能吧,有经验的立刻会想到,存在一个cache机制来确保每一个程序resource中的图片只被加载一次且只生成一个Drawable,并且A和B都引用了同一个Drawable。而setAlpha会改变背后的Drawable,于是A和B的外观会保持一样。

    那么这个cache是否能被关闭呢?看看API,提供了不少跟cache有关的方法哦,什么setDrawingCacheEnabled,什么setWillNotCacheDrawing,什么destroyDrawingCache,不过首先心一悬,这些方法都是来自View的,而且ImageView并未override;紧接着你进行试验,以上三个方法全部搭上,然后毫无效果。

    好吧,看源码吧,看这个cache到底是在哪进行的。看啊看,在ImageView源码的resolveUri函数中找到了答案,就是下面这句话:

    d = rsrc.getDrawable(mResource);

    这句话的意思是说,如果你是通过id引用drawable中的图片,那么会从rsrc(一个Resource对象)中来获取Drawable。于是明白了,是Resource进行了这个cache(哎早就该想到)。

    那怎么绕过这个cache呢?API不用指望了,Resource的这个cache行为粗看了下源码貌似是必须的(而且在大多数应用场景下是有利的)。所以应该对于需要修改图片透明度的ImageView,强制地创建新的Drawable。只讨论ImageView的Drawable为BitmapDrawable的情况,通过((BitmapDrawable)ImageView.getDrawable()).getBitmap()可以提取Bitmap,然后再new个新的BitmapDrawable并给ImageView重新setImageDrawable即可解决。

    当然更高效的做法,是对需要修改ImageView透明度的控件,实现一个你自己的Drawable缓存,只要这些ImageView不是同时出现在屏幕上即可。

  • 1、代替Timer的Trick   

          要定时更新UI首先会想到使用Timer+TimerTask+Handler,然而试验了半天发现TimerTask无法复用,就是每次使用前TimerTask需要重新创建。这点让人非常不爽,遂Google,从老外那觅得一简单高效的trick,原文地址:http://cart.kolix.de/?p=1438 。该trick的关键步骤是这样的:

    (1)首先创建一个Handler,并延迟将Runnable加入消息队列

    private Handler handler = new Handler();
    handler.postDelayed(runnable, 100);

    (2)然后创建如下格式的Runnable

    private Runnable runnable = new Runnable() {
       @Override
       public void run() {
          /* do what you need to do */
          foobar();
          /* and here comes the "trick" */
          handler.postDelayed(this, 100);
       }
    };

    (3)要清除队列中的Runnable时,调用handler.removeCallbacks(runnable)即可。

    -------------------------------------------n天后的分隔线-START-----------------------

    好了,经过实践,发现该trick有一个bug,对于postDelayed延时非常短的时候一般不会出现,但如果这个延时达到4、5秒的时候就会非常明显:

    虽然Runnable本身是运行在UI线程,并未开启新线程,按说不存在线程同步的问题,但postDelayed体现的非阻塞效果却证明系统背后一定用了另外一个线程来post(没有研究源码,如果谁发现我的想当然是错误的,请指出)。于是当post延时较长时就会出现bug:当你用handler.removeCallbacks来清除队列中的runnable时,队列很可能是空的,但这并不意味着没有runnable要post了,只是这个post还没到达而已。于是你虽然做了remove操作,runnable后面还是会继续运行,如果你后面再次调用handler.postDelay(runnable)的话,每次消息队列处理就会启动两个runnable,并随着你反复调用就会越来越多。

    解决办法:要彻底解决该bug需要两步操作:

    【1】在上述(2)的代码中,run函数在handler.postDelayed(this, 100);之前增加一行handler.removeCallbacks(this)。

    【2】增加一个shouldRun值来判断run函数是否该运行(也就是当你手动调用handler.removeCallbacks前将该值设为false),并在每次run函数开头check该值,如果为false则立刻退出。

    -------------------------------------------n天后的分隔线-END-----------------------

    2、细化Gallery对ItemSelectedChanged响应的trick

        用Gallery可以很方便地满足对horizontal listview这种控件的需求,而且它还有个很体贴的功能:setCallbackDuringFling(boolean)。因为一般用gallery都要考虑用户快速滑动的操作,这个时候selected item会频繁切换,如果你有个操作代价较高的callback去响应,则势必非常影响效率。而setCallbackDuringFling为false会启用Gallery中一个suppress item selected changed的功能,也就是当这个item selected changed为comfirmed的时候(也就是fling快速滑动停止后)才会调用callback。

         然而现在有这么个需求,为了让用户拥有滑动Gallery的爽快感,我需要有两种item selected changed事件:一种confirmed(setCallbackDuringFling为false);一种unconfirmed(setCallbackDuringFling为true )。我希望在前者的callback完成大数据量操作,在后者的callback完成迅速的外观上的改变(这样才爽)。

         这种事件的细分在public API上无法完成,于是我继承Gallery试图在protected级的方法上解决。然而研究试验了半天Gallery的源码,也没有找到合适的方法,而且还看到Gallery中一段诡异的源码(版本1.6):

        @Override
        void selectionChanged() {
            if (!mSuppressSelectionChanged) {
                super.selectionChanged();
            }
        }

        就是说Gallery竟然继承了父类一个私有方法?!完全不知道这个东西是怎么被成功编译的,反正我自己copy出来不能用。。这个就不管了,我再看啊看,终于找到了trick:getChildStaticTransformation这个protected的方法。

        这个方法从理解上来说可以想到,当Gallery需要呈现一个新的child view时,就需要调用这个方法。那么Gallery什么时候需要呈现新的child view?当然是Gallery滚动超过一个slot距离的时候,于是调用该方法的频率是大于等于unconfirmed item selected changed的频率的。于是后面不用说了,自己再暴露一个listener接口,然后继承上面的方法来callback(可以额外存个mSelectedPosition来判断保证不会冗余调用)。

        P.S. 经过实验后发现Gallery内部实现的对快速滑动时冗余selected item changed响应的suppress机制一点也不好用,经常会出现这种情况:你fling操作的前面稍微慢了一些,于是被识别成一个scroll+fling,于是前面的scroll会产生callback,于是这绝对不是我们想要的效果。最后我用上述的trick 1实现了selected item稳固了一定时间后才会产生callback的效果,效果很好。

  • 问题1:NDK的函数调用时出现 “UnsatisfiedLinkError : 函数名” 这样的异常

    解决办法:

    网上能找到的提醒无一不是让你去仔细检查NDK中的函数命名是否正确,也就是“JAVA_调用该函数的JAVA类名(完整路径区分大小写)_函数名”这样的JNI格式。然而我在确定这个命名准确无误后仍得到这个异常。

    为什么呢?反复捣鼓后幡然醒悟:我用的是C++(文件后缀用.cpp,按C++来编译)。

    由于JNI是采用了C风格的函数命名,所以如果用C++编译,则必须记得在每个函数前加上extern "C"的标记。这个东西记得NDK的docs里面有提到(现在找不到在哪了,那些docs只能自己一份份地看),实际写的时候还是忘了。

     

    问题2:进行jfieldID和jmethodID的cache时,不能转化为global reference

    解答:local/global reference这些概念,只是跟jobject的指针相关(包括它的派生类如jclass),因为这些指针所指空间,如果为local的话,则会在作用域结束时被回收,所以做cache时才需要转化为global reference。

    而jfieldID和jmethodID查看头文件便可知与jobject无关,只是一些native的空间分配,因此与local/global这些JNI加入的新概念无关,按C/C++的做法去cache即可。

     

    问题3:为何调用GetFieldID时,传入参数如果为类则必须按照这种格式:"Ljava/lang/String;",而调用FindClass时则直接“"java/lang/String"即可?

    解答:因为GetFieldID函数要解析类型签名,这些签名既包括int、bool这些原生类,也包括String或者自定义的JAVA类,因此需要对String这些需要指定类路径的设计一个额外的格式,以作为区分(估计可以提高解析签名的效率)。

    而FindClass函数不针对int、bool那些原生类(这些在C++中不算class),因此无需要加入这种格式上的区分。

     

    问题4:能够创建的local ref的数量是有限的吗?如果要创建一个比较大的jobjectArray并返回怎么办?

    解答:local ref的数量的确是有限的,这个限制大概比512小一点点(JNI背后是用一张表来记录你创建的每个local ref,这个表有长度限制),意味着你调用一个native函数时不能创建超过这个数量的local ref。

    于是你试图访问/返回一个超过该限制长度的jobjectArray时就很郁闷了,不过这是因为你对Get/SetObjectArrayElement这一对函数产生了一个误解。以SetObjectArrayElement为例,你传入一个local ref的参数,看上去是把这个local ref给放到了你的array中,实际上这个local ref并未放进去,放进去的只是ref指向的内容本身,所以这个函数执行后那个local ref就可以删除了,所以就不存在长度限制的问题。同样,GetObjectArrayElement每次会新产生一个local ref,所以你用完要立刻删掉,不然就可能超过local ref的数量限制。

    这个误解应该很普遍吧?其实Sun官方的JNI Spec文档中都有写,范例代码也都这么做了,不过还是强调得不够,第一次写的时候还是很容易被表面代码所误导。

     

    问题5:GetStringRegion和GetStringUTFRegion的特别提醒

    这两个函数都有一个len的参数,表示Unicode字符的个数,但是要注意,这两个函数会在copy完字符串后,还会操作数组的第len+1个元素(GetStringUTFRegion会把[len+1]赋值为0,但是GetStringRegion很奇怪,并不会把[len+1]赋为0)!因此分配空间时就要分配len+1,否则嘛。。你的程序随时crash(当你执行delete[]时)