Android加载View中Background详解
对大多数android的开发者来说,最经常的操作莫过于对界面进行布局,view中背景图片的加载是最经常做的。但是我们很少关注这个过程,这篇文章主要解析view中背景图片加载的流程。了解view中背景图片的加载(资源的加载)可以让我们对资源加载的过程进行一些优化,另外当需要进行整个应用的换肤时,也可以更得心应手。
view图片的加载,我们最常见的就是通过在xml文件当中进行drawable的设置,然后让android系统帮我们完成,或者手动写代码加载成bitmap,然后加载到view上。这篇文章主要分析android在什么时候以及怎么帮我们完成背景图片的加载的,那么我们就从activity.setcontentview还是layoutinflater.inflate(...)方法开始分析。
不管是从activity.setcontentview(...)还是layoutinflater.inflate(...)方法进行view的初始化,最终都会到达layoutinflater.inflate(xmlpullparser parser, viewgroup root, boolean attachtoroot)这个方法中。在这里我们主要关注view的背景图片加载,对于xml如何解析和加载就放过了。
public view inflate(xmlpullparser parser, viewgroup root, boolean attachtoroot) {
synchronized (mconstructorargs) {
final attributeset attrs = xml.asattributeset(parser);
context lastcontext = (context)mconstructorargs[0];
mconstructorargs[0] = mcontext;
view result = root;
try {
// look for the root node.
int type;
while ((type = parser.next()) != xmlpullparser.start_tag &&
type != xmlpullparser.end_document) {
// empty
}
if (type != xmlpullparser.start_tag) {
throw new inflateexception(parser.getpositiondescription()
+ ": no start tag found!");
}
final string name = parser.getname();
if (debug) {
system.out.println("**************************");
system.out.println("creating root view: "
+ name);
system.out.println("**************************");
}
if (tag_merge.equals(name)) {
if (root == null || !attachtoroot) {
throw new inflateexception("<merge /> can be used only with a valid "
+ "viewgroup root and attachtoroot=true");
}
rinflate(parser, root, attrs, false);
} else {
// temp is the root view that was found in the xml
view temp;
if (tag_1995.equals(name)) {
temp = new blinklayout(mcontext, attrs);
} else {
temp = createviewfromtag(root, name, attrs);
}
viewgroup.layoutparams params = null;
if (root != null) {
if (debug) {
system.out.println("creating params from root: " +
root);
}
// create layout params that match root, if supplied
params = root.generatelayoutparams(attrs);
if (!attachtoroot) {
// set the layout params for temp if we are not
// attaching. (if we are, we use addview, below)
temp.setlayoutparams(params);
}
}
if (debug) {
system.out.println("-----> start inflating children");
}
// inflate all children under temp
rinflate(parser, temp, attrs, true);
if (debug) {
system.out.println("-----> done inflating children");
}
// we are supposed to attach all the views we found (int temp)
// to root. do that now.
if (root != null && attachtoroot) {
root.addview(temp, params);
}
// decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachtoroot) {
result = temp;
}
}
} catch (xmlpullparserexception e) {
inflateexception ex = new inflateexception(e.getmessage());
ex.initcause(e);
throw ex;
} catch (ioexception e) {
inflateexception ex = new inflateexception(
parser.getpositiondescription()
+ ": " + e.getmessage());
ex.initcause(e);
throw ex;
} finally {
// don't retain static reference on context.
mconstructorargs[0] = lastcontext;
mconstructorargs[1] = null;
}
return result;
}
}
上面这么长一串代码,其实思路很清晰,就是针对xml文件进行解析,然后根据xml解析出的每一个节点进行view的初始化,紧接着将view的layout参数设置到view上,然后将view添加到它的父控件上。
为了了解view是怎么被加载出来的,我们只需要了解
temp = createviewfromtag(root, name, attrs);
跟进去看看。
/*
* default visibility so the bridgeinflater can override it.
*/
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;
}
}
上面代码的重点在于try...catch里的内容。try包起来的东西就是对view进行初始化,注意到上面代码中有几个factory,这些factory可以在view进行初始化,也就是说其实我们可以在这里干预view的初始化。从上面代码我们可以知道,如果我们自定义了一个factory,那么当前要初始化的view会优先被我们自定义的factory初始化,而不通过系统默认的factory初始化。那么如果我们要自定义factory,应该在哪里定义呢?容易想到,factory必须要赶在资源加载前自定义完成,所以我们应该在oncreate(...)的this.setcontentview(...)之前设置layoutinflater.factory。
getlayoutinflater().setfactory(factory);
接下来我们看到上面函数里面的
if (-1 == name.indexof('.')) {
view = oncreateview(parent, name, attrs);
} else {
view = createview(name, null, attrs);
}
这段函数就是对view进行初始化,有两种情况,一种是系统自带的view,它在
if (-1 == name.indexof('.'))
这里面进行初始化,因为如果是系统自带的view,传入的那么一般不带系统的前缀"android.view."。另一个分支初始化的是我们自定义的view。我们跟进oncreateview看看。
protected view oncreateview(string name, attributeset attrs)
throws classnotfoundexception {
return createview(name, "android.view.", attrs);
}
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;
final view view = constructor.newinstance(args);
if (view instanceof viewstub) {
// always use ourselves when inflating viewstub later
final viewstub viewstub = (viewstub) view;
viewstub.setlayoutinflater(this);
}
return view;
} 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 ? "<unknown>" : clazz.getname()));
ie.initcause(e);
throw ie;
}
}
从oncreateview(...)中我们知道,其实createviewfromtag(...)中对view的初始化最终都是通过createview(...)这个函数进行初始化的,不同只在于系统控件需要通过oncreateview(...)加上前缀,以便类加载器(classloader)正确地通过类所在的包初始化这个类。createview(...)这个函数的思路很清晰,不看catch里面的内容,try里面开头的两个分支就是用来将所要用的类构造函数提取出来,android系统会对使用过的类构造函数进行缓存,因为像textview这些常用的控件可能会被使用很多次。接下来,就是通过类构造函数对view进行初始化了。我们注意到传入构造函数的mconstructorargs是一个包含两个元素的数组。
final object[] mconstructorargs = new object[2];
那么我们就很清楚了,它就是调用系统控件中对应两个参数的构造函数。为了方便,我们就从最基础的view进行分析。
public view(context context, attributeset attrs) {
this(context, attrs, 0);
}
public view(context context, attributeset attrs, int defstyle) {
this(context);
typedarray a = context.obtainstyledattributes(attrs, com.android.internal.r.styleable.view,
defstyle, 0);
drawable background = null;
int leftpadding = -1;
int toppadding = -1;
int rightpadding = -1;
int bottompadding = -1;
int startpadding = undefined_padding;
int endpadding = undefined_padding;
int padding = -1;
int viewflagvalues = 0;
int viewflagmasks = 0;
boolean setscrollcontainer = false;
int x = 0;
int y = 0;
float tx = 0;
float ty = 0;
float rotation = 0;
float rotationx = 0;
float rotationy = 0;
float sx = 1f;
float sy = 1f;
boolean transformset = false;
int scrollbarstyle = scrollbars_inside_overlay;
int overscrollmode = moverscrollmode;
boolean initializescrollbars = false;
boolean leftpaddingdefined = false;
boolean rightpaddingdefined = false;
boolean startpaddingdefined = false;
boolean endpaddingdefined = false;
final int targetsdkversion = context.getapplicationinfo().targetsdkversion;
final int n = a.getindexcount();
for (int i = 0; i < n; i++) {
int attr = a.getindex(i);
switch (attr) {
case com.android.internal.r.styleable.view_background:
background = a.getdrawable(attr);
break;
case com.android.internal.r.styleable.view_padding:
padding = a.getdimensionpixelsize(attr, -1);
muserpaddingleftinitial = padding;
muserpaddingrightinitial = padding;
leftpaddingdefined = true;
rightpaddingdefined = true;
break;
//省略一大串无关的函数
}
由于我们只关注view中的背景图是怎么加载的,注意这个函数其实就是遍历attributeset attrs这个东西,然后对view的各个属性进行初始化。我们直接进入
background = a.getdrawable(attr);
这里看看(typedarray.getdrawable)。
public drawable getdrawable(int index) {
final typedvalue value = mvalue;
if (getvalueat(index*assetmanager.style_num_entries, value)) {
if (false) {
system.out.println("******************************************************************");
system.out.println("got drawable resource: type="
+ value.type
+ " str=" + value.string
+ " int=0x" + integer.tohexstring(value.data)
+ " cookie=" + value.assetcookie);
system.out.println("******************************************************************");
}
return mresources.loaddrawable(value, value.resourceid);
}
return null;
}
我们发现它调用mresources.loaddrawable(...),进去看看。
/*package*/ drawable loaddrawable(typedvalue value, int id)
throws notfoundexception {
if (trace_for_preload) {
// log only framework resources
if ((id >>> 24) == 0x1) {
final string name = getresourcename(id);
if (name != null) android.util.log.d("preloaddrawable", name);
}
}
boolean iscolordrawable = false;
if (value.type >= typedvalue.type_first_color_int &&
value.type <= typedvalue.type_last_color_int) {
iscolordrawable = true;
}
final long key = iscolordrawable ? value.data :
(((long) value.assetcookie) << 32) | value.data;
drawable dr = getcacheddrawable(iscolordrawable ? mcolordrawablecache : mdrawablecache, key);
if (dr != null) {
return dr;
}
drawable.constantstate cs = iscolordrawable
? spreloadedcolordrawables.get(key)
: (spreloadeddensity == mconfiguration.densitydpi
? spreloadeddrawables.get(key) : null);
if (cs != null) {
dr = cs.newdrawable(this);
} else {
if (iscolordrawable) {
dr = new colordrawable(value.data);
}
if (dr == null) {
if (value.string == null) {
throw new notfoundexception(
"resource is not a drawable (color or path): " + value);
}
string file = value.string.tostring();
if (trace_for_miss_preload) {
// log only framework resources
if ((id >>> 24) == 0x1) {
final string name = getresourcename(id);
if (name != null) android.util.log.d(tag, "loading framework drawable #"
+ integer.tohexstring(id) + ": " + name
+ " at " + file);
}
}
if (debug_load) log.v(tag, "loading drawable for cookie "
+ value.assetcookie + ": " + file);
if (file.endswith(".xml")) {
try {
xmlresourceparser rp = loadxmlresourceparser(
file, id, value.assetcookie, "drawable");
dr = drawable.createfromxml(this, rp);
rp.close();
} catch (exception e) {
notfoundexception rnf = new notfoundexception(
"file " + file + " from drawable resource id #0x"
+ integer.tohexstring(id));
rnf.initcause(e);
throw rnf;
}
} else {
try {
inputstream is = massets.opennonasset(
value.assetcookie, file, assetmanager.access_streaming);
// system.out.println("opened file " + file + ": " + is);
dr = drawable.createfromresourcestream(this, value, is,
file, null);
is.close();
// system.out.println("created stream: " + dr);
} catch (exception e) {
notfoundexception rnf = new notfoundexception(
"file " + file + " from drawable resource id #0x"
+ integer.tohexstring(id));
rnf.initcause(e);
throw rnf;
}
}
}
}
if (dr != null) {
dr.setchangingconfigurations(value.changingconfigurations);
cs = dr.getconstantstate();
if (cs != null) {
if (mpreloading) {
if (verifypreloadconfig(value, "drawable")) {
if (iscolordrawable) {
spreloadedcolordrawables.put(key, cs);
} else {
spreloadeddrawables.put(key, cs);
}
}
} else {
synchronized (mtmpvalue) {
//log.i(tag, "saving cached drawable @ #" +
// integer.tohexstring(key.intvalue())
// + " in " + this + ": " + cs);
if (iscolordrawable) {
mcolordrawablecache.put(key, new weakreference<drawable.constantstate>(cs));
} else {
mdrawablecache.put(key, new weakreference<drawable.constantstate>(cs));
}
}
}
}
}
return dr;
}
就是这个函数了,所有view的背景的加载都在这里了。这个函数的逻辑就比较复杂了,大体说来就是根据背景的类型(纯颜色、定义在xml文件中的,或者是一张静态的背景),如果缓存里面有,就直接用缓存里的。
总结一下,经过上面的分析,我们知道了,android就是在activity.setcontentview(...)中为我们进行资源文件的加载,精确到具体的函数的话,资源文件的加载就是在每一个被初始化的view的构造函数中进行加载的。
以上就是本文的全部内容了,希望对大家能够有所帮助。
推荐阅读