(Mis)-using itemRendererFunction and memory leaks

3

Category : DataGroup, Flex, List

One of the APIs that was added in Flex 4 to the new Spark List components was itemRendererFunction. It is really useful if you want to display different item renderers for different types of data.

If you use itemRendererFunction a lot, you might have noticed that virtualization doesn’t quite work as you’d expect. Only the on-screen item renderers are created, but item renderers aren’t re-used. That is until 4.6 when SDK-27727 was fixed.

However, as was soon found out when trying to use it, this can actually cause issues. Let’s look at a typical example of how to use itemRendererFunction:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx">
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayList;
            import mx.collections.IList;
 
            [Bindable("__NoChangeEvent__")]
            private var myNumbersDataProvider:IList = new ArrayList(
                    [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29]
                );
 
            private function myItemRendererFunction(item:Object):IFactory
            {
                var value:int = (item as int);
                var isEven:Boolean = (value % 2 == 0);
 
                if (isEven)
                {
                    return new ClassFactory(EvenItemRenderer);
                }
                else
                {
                    return new ClassFactory(OddItemRenderer);
                }
            }
        ]]>
    </fx:Script>
 
    <s:Scroller width="200" height="200" >
        <s:DataGroup dataProvider="{myNumbersDataProvider}"
                     itemRendererFunction="myItemRendererFunction">
            <s:layout>
                <s:VerticalLayout useVirtualLayout="true" horizontalAlign="contentJustify" gap="0" />
            </s:layout>
        </s:DataGroup>
    </s:Scroller>
</s:Application>

So what’s wrong with that? Well, if you run it in 4.5 or below, nothing really. The item renderers won’t be re-used, but everything works. However, if you run it in 4.6, you’ll notice that not only do your item renderers still not get re-used, but you’ve in-fact created a memory leak. As the user scrolls around, new item renderers get created, and all the old item renderers get stuck in memory.

In Flex 4.6, DataGroup was patched to recycle item renderers with an itemRendererFunction. However, it can’t just recycle all item renderers together because they are different (that is the point of the itemRendererFunction after all). So in order to figure out what item renderers can be recycled, it is based on what IFactory gets returned from your itemRendererFunction. To do that, it keeps a map of renderers to IFactories and a list of free item renderers by IFactory.

However, in this case, everytime it asks what IFactory to use to display a particular item, it gets back a new instance. In addition to this, DataGroup thinks it can re-use all the old item renderers, so they stay in-memory and hidden on the display list.

So how should you write your itemRendererFunctions? Well, rather than return a new instance of a ClassFactory everytime, you should return the same instance when possible. For the example above:

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" 
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/mx">
    <fx:Script>
        <![CDATA[
            import mx.collections.ArrayList;
            import mx.collections.IList;
 
            [Bindable("__NoChangeEvent__")]
            private var myNumbersDataProvider:IList = new ArrayList(
                    [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29]
                );
 
            private var evenItemRenderer:IFactory = new ClassFactory(EvenItemRenderer);
            private var oddItemRenderer:IFactory = new ClassFactory(OddItemRenderer);
 
            private function myItemRendererFunction(item:Object):IFactory
            {
                var value:int = (item as int);
                var isEven:Boolean = (value % 2 == 0);
 
                if (isEven)
                {
                    return evenItemRenderer;
                }
                else
                {
                    return oddItemRenderer;
                }
            }
        ]]>
    </fx:Script>
 
    <s:Scroller width="200" height="200" >
        <s:DataGroup dataProvider="{myNumbersDataProvider}"
                     itemRendererFunction="myItemRendererFunction">
            <s:layout>
                <s:VerticalLayout useVirtualLayout="true" horizontalAlign="contentJustify" gap="0" />
            </s:layout>
        </s:DataGroup>
    </s:Scroller>
</s:Application>

Even if you haven’t upgraded to 4.6 and don’t see this issue yet, it’s still a good idea to write your itemRendererFunctions this way. Unfortunately, there will be many people who upgrade to 4.6 without reading this blogpost, and all of a sudden their applications will start leaking memory. While one could say they’ve written their item renderer function incorrectly, the truth is that most people probably write their item renderer functions this way. It’s not necessarily a bug in the SDK, but given that so many people will face this issue (and it “breaks” on upgrading to 4.6), I think it’s something that should be addressed in the SDK.

Long-term, I’d like to see DataGroup expose an item renderer creation/recycling delegate, something similar to 4.5’s contentLoader concept. This would let people have more control over item renderer recycling. In addition, it would let people share item renderers across different DataGroup instance. However, short-term, I think we need something else.

I thought about adding in code just to limit the number of item renderers that get stored, but the way I decided to fix it was just to keep track of the number of item renderers that get created per item renderer factory. If more than one itemRenderer gets created for a particular factory, then we attempt to re-use item renderers for that factory–otherwise we just throw it away. This solution specifically handles this issue, without much overhead, and without much extra code. However, even after this, if you write your itemRendererFunction poorly, your item renderers still won’t be re-used (but atleast there will be no memory leak :-).

Because the Adobe Flex SDK JIRA has been exported already, I’ll wait until the Apache Flex SDK JIRA is up and running before submitting it as a patch. However, you can see the issue and the patch in this Flash Builder project.