Just An Application

September 7, 2014

The Mystery Of The Unsigned JAR: Part Three — The Little JAR That Hid

This is the bytecode for a method called load with everything removed except the calls to methods in the Java APIs.

As you can see, it is possible to deduce what the method is doing solely on the basis of the Java API method calls.


    public void load();
      Code:
         0: aload_0
         1: dup
         2: invokevirtual #6                  // Method java/lang/Object.getClass:()Ljava/lang/Class;
         
            ...
            
        11: invokevirtual #7                  // Method java/lang/Class.getResourceAsStream:(Ljava/lang/String;)Ljava/io/InputStream;
        
            ...
        
        47: new           #12                 // class java/util/jar/JarInputStream
        50: dup
        51: new           #13                 // class java/io/ByteArrayInputStream
        54: dup
        55: aload_1
        56: invokespecial #14                 // Method java/io/ByteArrayInputStream."<init>":([B)V
        59: invokespecial #15                 // Method java/util/jar/JarInputStream."<init>":(Ljava/io/InputStream;)V
        62: astore_2
        
            ...
            
        75: aload_2
        76: invokevirtual #16                 // Method java/util/jar/JarInputStream.getNextJarEntry:()Ljava/util/jar/JarEntry;
        79: dup
        80: astore        4
        82: ifnull        235
        85: aload         4
        87: invokevirtual #17                 // Method java/util/jar/JarEntry.getName:()Ljava/lang/String;
        90: dup
        91: astore        5
        
            ...
            
        99: invokevirtual #18                 // Method java/lang/String.endsWith:(Ljava/lang/String;)Z
       102: ifeq          172
       
            ...
            
       117: iconst_0
       118: iconst_1
       119: dup
       120: pop2
       121: aload         5
       123: dup_x1
       124: invokevirtual #19                 // Method java/lang/String.length:()I
       127: bipush        6
       129: iconst_1
       130: dup
       131: pop2
       132: isub
       133: invokevirtual #20                 // Method java/lang/String.substring:(II)Ljava/lang/String;
       136: bipush        47
       138: iconst_1
       139: dup
       140: pop2
       141: bipush        46
       143: iconst_1
       144: dup
       145: pop2
       146: invokevirtual #21                 // Method java/lang/String.replace:(CC)Ljava/lang/String;
       149: astore        5
       
            ...
            
       163: aload_3
       164: invokevirtual #22                 // Method java/util/HashMap.put:(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
       167: pop
       168: goto          75
       171: pop
       172: aload         4
       174: invokevirtual #23                 // Method java/util/jar/JarEntry.isDirectory:()Z
       177: ifeq          185
       180: aload_2
       181: goto          76
      
            ...

The calls to the JarInputStream and JarEntry methods make it clear that the method is iterating over the contents of a JAR

The 6 in

    127: bipush        6

just happens to be the length of the suffix

    .class

The 47 in

    136: bipush        47

is the solidus character (‘/‘) and the 46 in

    141: bipush        46

is the dot character (‘.‘).

The sequences following the iconst_0 instruction and each bipush instruction

    117: iconst_0
    118: iconst_1
    119: dup
    120: pop2
    
    ...

    127: bipush        6
    129: iconst_1
    130: dup
    131: pop2
    
    ...
    
    136: bipush        47
    138: iconst_1
    139: dup
    140: pop2
    
    ...

    141: bipush        46
    143: iconst_1
    144: dup
    145: pop2

are all either pretty feeble attempts at obfuscation or evidence of a severely confused compiler as they have no effect at all.

From time immemorial Java class loaders have been converting the JarEntry names of class files into class names and that is what this method is doing as well.

If you have a class, for example,

    xper.mcm.CrashPow

then the name of the JarEntry for the class file in a JAR will be

    xper/mcm/CrashPow.class

Chop off the .class suffix

    117: iconst_0

      0
      
    118: iconst_1
    
      0, 1
      
    119: dup
    
      0, 1, 1

    120: pop2
    
      0
      
    121: aload         5
    
      0, entry_name
      
    123: dup_x1
    
      entry_name, 0, entry_name
      
    124: invokevirtual #19                 // Method java/lang/String.length:()I
    
      entry_name, 0, length(entry_name)
      
    127: bipush        6
    
      entry_name, 0, length(entry_name), 6
      
    129: iconst_1
    
      entry_name, 0, length(entry_name), 6, 1
      
    130: dup
    
      entry_name, 0, length(entry_name), 6, 1, 1
    
    131: pop2
    
      entry_name, 0, length(entry_name), 6
    
    132: isub
    
      entry_name, 0, length(entry_name)_minus_6
      
    133: invokevirtual #20                 // Method java/lang/String.substring:(II)Ljava/lang/String;

      substring(entry_name, 0, length(entry_name)_minus_6))

and replace the occurrences of ‘/‘ with ‘.

    136: bipush        47
    
      substring(...), 47
      
    138: iconst_1
    
      substring(...), 47, 1
      
    139: dup
    
      substring(...), 47, 1, 1
      
    140: pop2
    
      substring(...), 47
      
    141: bipush        46
    
       substring(...), 47, 46
       
    143: iconst_1
    
      substring(...), 47, 46, 1
      
    144: dup
    
      substring(...), 47, 46, 1, 1

    145: pop2
    
      substring(...), 47, 46
       
    146: invokevirtual #21                 // Method java/lang/String.replace:(CC)Ljava/lang/String;
    
      substring(...).replace(47, 46)
        
    149: astore        5

and you get the canonical Java class name back again

The method is storing the contents of the class files in the JAR it is iterating over in a HashMap with the class names as the keys.

Now where’s the JAR ?


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

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

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.