一次曲折的调试经历

前不久在FlexLite的开发者群里面遇到一个开发者的提问,说是在使用List的时候水平滚动条的大小有错误。截图大概是下面这样:

可以看到水平滚动条的宽度是不对的。经过简单的了解,发现代码没错。本着(好)助(奇)人(害)为(死)乐(猫)的精神,决定找出问题所在。我简化代码,写了个DEMO,也复现了这个问题。代码大致如下只有两个类:
主类:

public function FlexLiteTest()
{
    super();
    Injector.mapClass(Theme, VectorTheme);
    Debugger.initialize(stage);
}

override protected function createChildren():void
{
    var array:Array = [];
    for (var i:int = 0; i < 20; i++) 
    {
        array.push(i);
    }
    var list:List = new List();
    list.x = 10;
    list.y = 10;
    list.skinName = MyListSkin;
    list.width = 400;
    var hLayout:HorizontalLayout = new HorizontalLayout();
    hLayout.verticalAlign = VerticalAlign.CONTENT_JUSTIFY; 
    list.layout = hLayout;
    list.dataProvider = new ArrayCollection(array);
    this.addElement(list);
}

List皮肤类:

package
{
    import org.flexlite.domUI.skins.vector.ListSkin;

    /**
     * @author xzper
     */
    public class MyListSkin extends ListSkin
    {
        public function MyListSkin()
        {
            super();
        }

        override protected function createChildren():void
        {
            super.createChildren();
            scroller.horizontalScrollBar.skinName = MyHScrollBarSkin;
        }
    }
}
import org.flexlite.domUI.skins.vector.HScrollBarSkin;

class MyHScrollBarSkin extends HScrollBarSkin
{
    override protected function createChildren():void
    {
        super.createChildren();
    }
}

代码大意很简单,就是想换一下List的水平滚动条的皮肤,有不想写重复代码于是就是继承自ListSkin和HScrollBarSkin写了个皮肤。

经过简单的一番排查,发现注释掉

list.skinName = MyListSkin; 

或者

scroller.horizontalScrollBar.skinName = MyHScrollBarSkin; 

最后的结果是没问题的,如下图:

那么问题肯定就出在了那个滚动条皮肤上面了。细看代码,发现其实滚动条皮肤就重写了一个createChildren的方法,啥也没干。此时思维陷入僵局…看来此路不通。

既然是显示结果的问题,那么换种思路,从结果入手,滚动条的宽度比预期值小了很多,通过FlexLite框架自带的一个调试工具,可以清楚地看到运行时各个组件大小位置的实际值测量值和布局结果。调试工具需要在项目开始初始化

Debugger.initialize(stage);

然后默认按F11开启。通过调试面板定位到滚动条,如下图:

发现水平滚动条的宽度是对的,布局宽为400,但是皮肤的宽是错的。
因为FlexLite里面有两种皮肤,一种是Skin可显示对象版的皮肤和StateSkin非显示对象皮肤。这里用到的是继承自Skin的可显示对象皮肤。布局的宽和测量宽是一样的都是86。

那么问题就出在这里了。滚动条皮肤的布局宽(layoutBoundsWidth)是错误的,理应该也是400。这时可以回到代码了,在适当的地方下断点,找到原因,问题就能解决了。

我给重写了下MyHScrollBarSkin的setLayoutBoundsSize,并在这里下断点。这样可以找出什么地方给MyHScrollBarSkin设置了一个错误的布局尺寸。

启动程序。发现这个断点根本就进不去,也就是说没有任何地方设置水平滚动条的布局尺寸,那么滚动条的布局尺寸是哪里来的呢,通过UIComponent的源码发现,没有设置布局尺寸的话,通过getter方法获取到的layoutBoundsWidth值就是explicitWidth或者measuredWidth,显然这地方是使用的measuredWidth。看到这里,问题就变成了setLayoutBoundsSize这个方法为何没有被调用。

通过查看调用层次结构,发现本来setLayoutBoundsSize应该在UIAssets的updateDisplayList方法里面被调用的(因为SkinnableComponent继承自UIAssets,刷新显示列表的时候会设置皮肤的布局属性)。于是我在UIAssets的updateDisplayList方法里面下断点,尝试找到皮肤没有被布局的原因。由于updateDisplayList会被反复调用,为了更精确我给断点加了条件表达式这样会少很多麻烦。

因为只有滚动条才有value属性,用这个表达式可以确保断点每次进入时都是滚动条触发的。经过几次断点,发现问题是出了在一个scaleSkin的实例变量上面。这个变量不知是何原因变成了false,导致条件判断不通过,导致皮肤没有被布局。

答案似乎近在眼前了,scaleSkin被设置成了false导致皮肤没有跟着主机组件进行缩放。那么找到设置scaleSkin这个值为false的位置就行了。理所当然的,我使用调用层次结构和查找引用的功能企图找到项目中设置这个值的地方,然而得到的结果却是除了初始化给这个属性赋值了一次true以外,没有任何地方给这个属性重新赋值。甚至使用了Ctrl+H全局搜索这个scaleSkin的字符串得到结果也是一样。这个结果难免让我无法接受。scaleSkin被莫名其妙地改了值?

既然这样,我决定改一下UIAssets的代码,将scaleSkin的定义方式从public的声明方式改为getter/setter方法,这样可以下断点调试何时设置了scaleSkin的值。

private var _scaleSkin:Boolean = true;
/**
 * 是否缩放皮肤
 */
public function get scaleSkin():Boolean
{
    return _scaleSkin;
}
public function set scaleSkin(value:Boolean):void
{
    _scaleSkin = value;
}

编译,运行。还没来得及设置断点,结果让我又惊有喜。这次运行结果居然是对的,我有点不敢相信,再次编译运行,结果还是对的。改回去,结果就是错的了。

心中暗骂,这他妈是什么gui。Flash编译器的bug????反正不管怎样,这样写没错就是了,于是将解决方案告之。但是我心中总感觉似乎有哪里不对劲,难到这真的是编译器的bug?那么为什么将赋值皮肤的那一行去掉结果也没问题呢?

就在我神情恍惚之际,我一步一步地断点调试。发现一开始scaleSkin确实是true,然后某一个步骤之后scaleSkin就变成了false。我开始逐渐缩小这个区间,最后将目标锁定到了SkinnableComponent的这个函数里面

/**
 * 卸载皮肤
 */        
protected function detachSkin(skin:Object):void
{       
    if(hasCreatedSkinParts)
    {
        removeSkinParts();
        hasCreatedSkinParts = false;
    }
    if(skin is ISkin)
    {
        var skinParts:Vector.<String> = SkinPartUtil.getSkinParts(this);
        for each(var partName:String in skinParts)
        {
            if(!(partName in this))
                continue;
            if (this[partName] != null)
            {
                partRemoved(partName,this[partName]);
            }
            this[partName] = null;
        }
        (skin as ISkin).hostComponent = null;
    }
}

我发现在for each某一个循环之后,scaleSkin发生了变化。同时通过断点发现skinParts居然还包含了一个scaleSkin的值

果然罪魁祸首找到了,就是这里。使用[]的方式对属性进行赋值,逃过了查找引用和全局搜索。真是天网恢恢,总会漏那么一点。

在卸载旧皮肤的时候(因为在ListSkin里面HScroller添加到显示列表会有一个主题皮肤,然后在MyListSkin会又设置一次HScroller的皮肤,所以会触发卸载皮肤的方法)框架内部将scaleSkin当成了皮肤部件,使用了 this[“scaleSkin”] = null 的方式改变了这个属性的值。

那么新的问题又产生了,scaleSkin怎么会成为了皮肤部件?通过查看SkinPartUtil的getSkinParts方法

不难发现第47行,没有使用toString(),导致判断永远成立,这个类里面的所有public的属性都被当做了皮肤部件。解决办法就是加上toString()。

if(basicTypes.indexOf(node.@type.toString())==-1)

另外SkinnableComponent在卸载皮肤校验时,没有判断部件是否也存在与皮肤中,可以将

if(!(partName in this))
    continue;

改为

if(!(partName in this)||!(partName in this.skin))
    continue;

到这里问题才算是彻底解决了。遇到问题还是需要刨根问底,不然永远只能停留在表面上。
最后附上错误代码的DEMO项目,留个纪念

http://xzper.qiniudn.com/2015/05/FlexLiteTest.zip