Just An Application

December 18, 2011

Two Android Applications, A Shared UI Element, And A Shared Process: A Tale Of ClassLoaders And Confusion

1. Sharing UI Elements Between Android Applications

Suppose I have a sub-class of android.view.View called xper.common.CommonView which I want to use in the main layouts of two applications XperGeraint and XperTristram like this

    <?xml version="1.0" encoding="utf-8"?>
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:orientation="vertical" >

            <TextView
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:text="@string/hello" />
            <xper.common.CommonView 
                android:id="@+id/common_view"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"/>
        </LinearLayout>

I want the main activity of each application to access the instance of xper.common.CommonView in the main layout in its onCreate() method using the findViewById() like this.

    CommonView cv = (CommonView)findViewById(R.id.common_view);

Well I can do that quite easily. I have to have a copy of the code for xper.common.CommonView in each application but everything works as expected.

2. Running Two Android Applications With A Shared UI Element In The Same Process: The Plot Thickens

Then for some reason I decide that both applications need to run in the same process so I modify the application manifests so they have the same shared UID and process attributes like this

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="xper.geraint"
        android:versionCode="1"
        android:versionName="1.0" 
        android:sharedUserId="xper.uid.shared">

        <uses-sdk android:minSdkVersion="14"/>

        <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:process="xper.process.shared">
            <activity
                android:label="@string/app_name"
                android:name=".XperGeraintActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>

                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
        </application>

    </manifest>

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="xper.tristram"
        android:versionCode="1"
        android:versionName="1.0" 
        android:sharedUserId="xper.uid.shared">

        <uses-sdk android:minSdkVersion="14"/>

        <application
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name" 
            android:process="xper.process.shared">
            <activity
                android:label="@string/app_name"
                android:name=".XperTristramActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN"/>

                    <category android:name="android.intent.category.LAUNCHER"/>
                </intent-filter>
            </activity>
        </application>

    </manifest>

3. Whoops ! Things Take A Turn For The Worse

Then I run XperGeraint and then XperTristram and this happens (output slightly re-formatted for clarity)

    ...

    E/AndroidRuntime(  797): java.lang.RuntimeException: Unable to start activity \
        ComponentInfo{xper.tristram/xper.tristram.XperTristramActivity}: \
        java.lang.ClassCastException: xper.common.CommonView cannot be cast to xper.common.CommonView
    E/AndroidRuntime(  797): 	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1955)
    E/AndroidRuntime(  797): 	at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:1980)
    E/AndroidRuntime(  797): 	at android.app.ActivityThread.access$600(ActivityThread.java:122)
    E/AndroidRuntime(  797): 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1146)
    E/AndroidRuntime(  797): 	at android.os.Handler.dispatchMessage(Handler.java:99)
    E/AndroidRuntime(  797): 	at android.os.Looper.loop(Looper.java:137)
    E/AndroidRuntime(  797): 	at android.app.ActivityThread.main(ActivityThread.java:4340)
    E/AndroidRuntime(  797): 	at java.lang.reflect.Method.invokeNative(Native Method)
    E/AndroidRuntime(  797): 	at java.lang.reflect.Method.invoke(Method.java:511)
    E/AndroidRuntime(  797): 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
    E/AndroidRuntime(  797): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
    E/AndroidRuntime(  797): 	at dalvik.system.NativeStart.main(Native Method)
    E/AndroidRuntime(  797): Caused by: java.lang.ClassCastException: \
        xper.common.CommonView cannot be cast to xper.common.CommonView
    E/AndroidRuntime(  797): 	at xper.tristram.XperTristramActivity.onCreate(XperTristramActivity.java:29)
    E/AndroidRuntime(  797): 	at android.app.Activity.performCreate(Activity.java:4465)
    E/AndroidRuntime(  797): 	at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1049)
    E/AndroidRuntime(  797): 	at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:1919)
    E/AndroidRuntime(  797): 	... 11 more
	
    ...

which is somewhat unexpected.

4. Enter A Pair Of ClassLoaders

The most likely explanation for the at first glance rather baffling ClassCastException is that there are two instances of the class xper.common.CommonView loaded by two different ClassLoaders involved.

By adding some debug code we can see that this is true for the case when we get the ClassCastException

This

        ...

        setContentView(R.layout.main);
        
        System.out.println("GeraintActivity: CommonView.class.getClassLoader() == " + CommonView.class.getClassLoader());
        
        View v  = findViewById(R.id.common_view);
        
        System.out.println("GeraintActivity: v.getClass().getClassLoader() == " + v.getClass().getClassLoader());
        
        CommonView cv = (CommonView)findViewById(R.id.common_view);
        
        System.out.println("GeraintActivity: cv == " + cv);
	
        ...

produces

    ...

   I/System.out( 1047): GeraintActivity: CommonView.class.getClassLoader() == dalvik.system.PathClassLoader[/data/app/xper.geraint-2.apk]
   I/System.out( 1047): GeraintActivity: v.getClass().getClassLoader() == dalvik.system.PathClassLoader[/data/app/xper.geraint-2.apk]
   I/System.out( 1047): GeraintActivity: cv == xper.common.CommonView@413548b0

   ...

whereas this

        ...

        setContentView(R.layout.main);
        
        System.out.println("TristramActivity: CommonView.class.getClassLoader() == " + CommonView.class.getClassLoader());
        
        View       v  = findViewById(R.id.common_view);
        
        System.out.println("TristramActivity: v.getClass().getClassLoader() == " + v.getClass().getClassLoader());
        
        CommonView cv = (CommonView)findViewById(R.id.common_view);
        
        System.out.println("TristramActivity: cv == " + cv);
		
        ...

produces

    ...

    I/System.out( 1047): TristramActivity: CommonView.class.getClassLoader() == dalvik.system.PathClassLoader[/data/app/xper.tristram-2.apk]
    I/System.out( 1047): TristramActivity: v.getClass().getClassLoader() == dalvik.system.PathClassLoader[/data/app/xper.geraint-2.apk]
	
    ...

followed by a ClassCastException.

So we can see that the Java runtime never lies. In the second case the assignment statement

        ...

        CommonView cv = (CommonView)findViewById(R.id.common_view);

        ...

is indeed attempting to cast between incompatible types.

The question is what is responsible for creating the instance of xper.common.CommonView during the inflation of the layout and why does it use that particular ClassLoader ?

5. Layout Inflation

In each Activity the main layout is inflated as a result of a call to the inherited method Activity.setContentView(int layoutResID) which in turn calls

    getWindow().setContentView(layoutResID)

The Activity.getWindow() method returns the instance variable mWindow which is of type android.view.Window.

The Window.setContentView(int layoutResID) method is declared abstract so we need to know exactly what the class of the object referred to by mWindow is in order to know how the layout is being inflated.

The instance of android.view.Window referenced by mWindow is the result of the following call

    PolicyManager.makeNewWindow(this);

The easiest way to work out exactly what is being returned is to add some more debug output.

Adding

        ...

        System.out.println("GeraintActivity: getWindow() == " + getWindow());
	
        ...

gives us

    ...

    I/System.out(  868): GeraintActivity: getWindow() == com.android.internal.policy.impl.PhoneWindow@41348dc8
	
    ...

and adding

        ...

        System.out.println("TristramActivity: getWindow() == " + getWindow());

        ...

gives us

    ...

    I/System.out(  868): TristramActivity: getWindow() == com.android.internal.policy.impl.PhoneWindow@41359cf0
	
    ...

Each Activity has a reference to a distinct instance of com.android.internal.policy.impl.PhoneWindow.

6. PhoneWindow

The PhoneWindow.setContentView(int) method is defined as follows.

File: frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java [244-256]

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }

The mLayoutInflater instance variable is a reference to an object of type android.view.LayoutInflater.

The LayoutInflater instance is created in the PhoneWindow‘s constructor using the call

    LayoutInflater.from(context);

where context is the Activity instance.

This method in turn calls

    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

7. getSystemService(String)

The Activity.getSystemService(String) method is defined as follows

File: frameworks/base/core/java/android/app/Activity.java [3986-4000]

    @Override
    public Object getSystemService(String name) {
        if (getBaseContext() == null) {
            throw new IllegalStateException(
                    "System services not available to Activities before onCreate()");
        }

        if (WINDOW_SERVICE.equals(name)) {
            return mWindowManager;
        } else if (SEARCH_SERVICE.equals(name)) {
            ensureSearchManager();
            return mSearchManager;
        }
        return super.getSystemService(name);
    }

The super class of Activity is android.view.ContextThemeWrapper and its implementation of the getSystemService() method is defined as follows

File: frameworks/base/core/java/android/view/ContextThemeWrapper.java [72-80]

    @Override public Object getSystemService(String name) {
        if (LAYOUT_INFLATER_SERVICE.equals(name)) {
            if (mInflater == null) {
                mInflater = LayoutInflater.from(mBase).cloneInContext(this);
            }
            return mInflater;
        }
        return mBase.getSystemService(name);
    }

8. The Return Of The LayoutInflater

We already know what the method LayoutInflater.from(Context) does but what about LayoutInflater.cloneInConrext(Context) ?

It is declared to be abstract in the class LayoutInflater itself but we know that whatever it returns is returned by the original call to the method getSystemService(String) on the Activity.

We can do the same thing in each Activity’s onCreate() method and see what we get back.

Adding

        ...

        LayoutInflater li = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE);
        
        System.out.println("GeraintActivity: li == " + li);
	
        ...

produces

    ...

    I/System.out(  588): GeraintActivity: li == com.android.internal.policy.impl.PhoneLayoutInflater@41362750
	
    ...

and adding

        ...

        LayoutInflater li = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE);
        
        System.out.println("TristramActivity: li == " + li);
	
        ...

produces

    ...

    I/System.out(  588): TristramActivity: li == com.android.internal.policy.impl.PhoneLayoutInflater@4134ff18
	
    ...

So the PhoneWindow for each Activity uses an instance of com.android.internal.policy.impl.PhoneLayoutInflater to inflate that Activity’s layout.

9. The PhoneLayoutInflater

The PhoneLayoutInflater class implements the LayoutInflater.cloneInConrext() method as required and overrides the onCreateView() method as follows

File: frameworks/base/policy/src/com/android/internal/policy/impl/PhoneLayoutInflater.java [53-67]

   @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }

The static variable sClassPrefixList contains the Strings "android.widget." and "android.webkit." so even if this method were to come into play in the xper.common.CommnView case it is simply going to call the super class implementation.

In short it looks as though whatever is responsible for creating the instance of xper.common.CommonView is somewhere in the implementation of the LayoutInflater class itself.

10. LayoutInflater.inflate(int, View)

The PhoneWindow instance calls the method LayoutInflater.inflate(int, View) on its PhoneLayoutInflater instance to inflate the Activity’s layout.

This method is a wrapper method which calls further wrapper methods.

The sequence eventually bottoms out in a call to the method

    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

This method calls the method

    void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate)

to inflate the children of the root View.

This method calls the method

    View createViewFromTag(View parent, String name, AttributeSet attrs)

to create the child View then calls itself on that child’s children.

So it is the method createViewFromTag(View, String, AttributeSe) which is creating the instance of xper.common.CommnView.

11. LayoutInflater.createViewFromTag()

File: frameworks/base/core/java/android/view/LayoutInflater.java […]

     View createViewFromTag(View parent, String name, AttributeSet attrs) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        if (DEBUG) System.out.println("******** Creating view: " + name);

        try {
            View view;
            if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
            else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
            else view = null;

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
            }
            
            if (view == null) {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            }

            if (DEBUG) System.out.println("Created view is: " + view);
            return view;

        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name);
            ie.initCause(e);
            throw ie;

        } catch (Exception e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name);
            ie.initCause(e);
            throw ie;
        }
    }

If the instance variables

  • mFactory
  • mFactory2
  • mPrivateFactory

are all null, which by default they appear to be, then when this method is called on the XML node with the tag xper.common.CommonView
it is the else part of the statement

            ...

            if (-1 == name.indexOf('.')) {
                view = onCreateView(parent, name, attrs);
            } else {
                view = createView(name, null, attrs);
            }
		
            ...

which will be executed.

12. LayoutInflater.createView()

The LayoutInflater.createView() method is defined as follows

File: frameworks/base/core/java/android/view/LayoutInflater.java [544-612]

     public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
                sConstructorMap.put(name, constructor);
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = mContext.getClassLoader().loadClass(
                                prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        
                        boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                        mFilterMap.put(name, allowed);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    } else if (allowedState.equals(Boolean.FALSE)) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
            }

            Object[] args = mConstructorArgs;
            args[1] = attrs;
            return constructor.newInstance(args);

        } catch (NoSuchMethodException e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class "
                    + (prefix != null ? (prefix + name) : name));
            ie.initCause(e);
            throw ie;

        } catch (ClassCastException e) {
            // If loaded class is not a View subclass
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Class is not a View "
                    + (prefix != null ? (prefix + name) : name));
            ie.initCause(e);
            throw ie;
        } catch (ClassNotFoundException e) {
            // If loadClass fails, we should propagate the exception.
            throw e;
        } catch (Exception e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class "
                    + (clazz == null ? "" : clazz.getName()));
            ie.initCause(e);
            throw ie;
        }
    }

As the comment states, and on this occasion the comments and the code are in sync, this method is using a cache.

For each View sub-class it is asked to construct it caches the constructor object that it acquires using reflection.

This is not inherently a bad idea, Java reflection operations can be quite slow, unfortunately it is a global cache. As the name suggests sConstructorMap is a static variable and the constructors are cached by name.

Hence if an instance of a View sub-class is constructed in the context of a given ClassLoader then the constructor is cached, but as we have seen, that constructor can then end up being used in the context of a completely different ClassLoader.

13. A Work-around: Exit Pursued By A Bear

There is a way to work-around this problem. Use the LayoutInflater.setFactory() method to over-ride the creation of the shared Views by the LayoutInflater.

For example this is a simple non-caching implementation of the LayoutInflater.Factory interface.

    package xper.common;

    import java.lang.reflect.Constructor;

    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.LayoutInflater;
    import android.view.View;

    public class XperLayoutInflaterFactory 
                 implements
                     LayoutInflater.Factory
    {
        public static LayoutInflater.Factory getInstance()
        {
            return INSTANCE;
        }
	
        //
	
        public View onCreateView(String name, Context context, AttributeSet attrs) 
        {
            if (name.startsWith(PACKAGE_PREFIX))
            {
                try
                {
                    Class<? extends View>       viewSubClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
                    Constructor<? extends View> constructor  = viewSubClass.getConstructor(CONSTRUCTOR_SIGNATURE);
					
                    return constructor.newInstance(new Object[]{context, attrs});
                }
                catch (ClassNotFoundException cnfe)
                {
                }
                catch (NoSuchMethodException nsme) 
                {
                } 
                catch (Exception e)
                {
                } 
            }
            return null;
        }

        //
	
        private static XperLayoutInflaterFactory INSTANCE = new XperLayoutInflaterFactory();
	
        //
	
        private static final Class[] CONSTRUCTOR_SIGNATURE = new Class[] {Context.class, AttributeSet.class};
	
        //
	
        private static final String PACKAGE_PREFIX = "xper.common.";

    }

If you are not inflating large numbers of shared Views then the lack of caching is unlikely to be an issue.

Writing a caching version is left as an exercise for the reader.


Copyright (c) 2011 By Simon Lewis. All Rights Reserved.

Blog at WordPress.com.