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.