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(XmlPullParserparser,ViewGrouproot,booleanattachToRoot)这个方法中。在这里我们主要关注View的背景图片加载,对于XML如何解析和加载就放过了。
publicViewinflate(XmlPullParserparser,ViewGrouproot,booleanattachToRoot){ synchronized(mConstructorArgs){ finalAttributeSetattrs=Xml.asAttributeSet(parser); ContextlastContext=(Context)mConstructorArgs[0]; mConstructorArgs[0]=mContext; Viewresult=root; try{ //Lookfortherootnode. inttype; while((type=parser.next())!=XmlPullParser.START_TAG&& type!=XmlPullParser.END_DOCUMENT){ //Empty } if(type!=XmlPullParser.START_TAG){ thrownewInflateException(parser.getPositionDescription() +":Nostarttagfound!"); } finalStringname=parser.getName(); if(DEBUG){ System.out.println("**************************"); System.out.println("Creatingrootview:" +name); System.out.println("**************************"); } if(TAG_MERGE.equals(name)){ if(root==null||!attachToRoot){ thrownewInflateException("<merge/>canbeusedonlywithavalid" +"ViewGrouprootandattachToRoot=true"); } rInflate(parser,root,attrs,false); }else{ //Tempistherootviewthatwasfoundinthexml Viewtemp; if(TAG_1995.equals(name)){ temp=newBlinkLayout(mContext,attrs); }else{ temp=createViewFromTag(root,name,attrs); } ViewGroup.LayoutParamsparams=null; if(root!=null){ if(DEBUG){ System.out.println("Creatingparamsfromroot:"+ root); } //Createlayoutparamsthatmatchroot,ifsupplied params=root.generateLayoutParams(attrs); if(!attachToRoot){ //Setthelayoutparamsfortempifwearenot //attaching.(Ifweare,weuseaddView,below) temp.setLayoutParams(params); } } if(DEBUG){ System.out.println("----->startinflatingchildren"); } //Inflateallchildrenundertemp rInflate(parser,temp,attrs,true); if(DEBUG){ System.out.println("----->doneinflatingchildren"); } //Wearesupposedtoattachalltheviewswefound(inttemp) //toroot.Dothatnow. if(root!=null&&attachToRoot){ root.addView(temp,params); } //Decidewhethertoreturntherootthatwaspassedinorthe //topviewfoundinxml. if(root==null||!attachToRoot){ result=temp; } } }catch(XmlPullParserExceptione){ InflateExceptionex=newInflateException(e.getMessage()); ex.initCause(e); throwex; }catch(IOExceptione){ InflateExceptionex=newInflateException( parser.getPositionDescription() +":"+e.getMessage()); ex.initCause(e); throwex; }finally{ //Don'tretainstaticreferenceoncontext. mConstructorArgs[0]=lastContext; mConstructorArgs[1]=null; } returnresult; } } 上面这么长一串代码,其实思路很清晰,就是针对XML文件进行解析,然后根据XML解析出的每一个节点进行View的初始化,紧接着将View的Layout参数设置到View上,然后将View添加到它的父控件上。 为了了解View是怎么被加载出来的,我们只需要了解 temp=createViewFromTag(root,name,attrs); 跟进去看看。 /* *defaultvisibilitysotheBridgeInflatercanoverrideit. */ ViewcreateViewFromTag(Viewparent,Stringname,AttributeSetattrs){ if(name.equals("view")){ name=attrs.getAttributeValue(null,"class"); } if(DEBUG)System.out.println("********Creatingview:"+name); try{ Viewview; if(mFactory2!=null)view=mFactory2.onCreateView(parent,name,mContext,attrs); elseif(mFactory!=null)view=mFactory.onCreateView(name,mContext,attrs); elseview=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("Createdviewis:"+view); returnview; }catch(InflateExceptione){ throwe; }catch(ClassNotFoundExceptione){ InflateExceptionie=newInflateException(attrs.getPositionDescription() +":Errorinflatingclass"+name); ie.initCause(e); throwie; }catch(Exceptione){ InflateExceptionie=newInflateException(attrs.getPositionDescription() +":Errorinflatingclass"+name); ie.initCause(e); throwie; } }
上面代码的重点在于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看看。
protectedViewonCreateView(Stringname,AttributeSetattrs) throwsClassNotFoundException{ returncreateView(name,"android.view.",attrs); } publicfinalViewcreateView(Stringname,Stringprefix,AttributeSetattrs) throwsClassNotFoundException,InflateException{ Constructor<?extendsView>constructor=sConstructorMap.get(name); Class<?extendsView>clazz=null; try{ if(constructor==null){ //Classnotfoundinthecache,seeifit'sreal,andtrytoaddit clazz=mContext.getClassLoader().loadClass( prefix!=null?(prefix+name):name).asSubclass(View.class); if(mFilter!=null&&clazz!=null){ booleanallowed=mFilter.onLoadClass(clazz); if(!allowed){ failNotAllowed(name,prefix,attrs); } } constructor=clazz.getConstructor(mConstructorSignature); sConstructorMap.put(name,constructor); }else{ //Ifwehaveafilter,applyittocachedconstructor if(mFilter!=null){ //Haveweseenthisnamebefore? BooleanallowedState=mFilterMap.get(name); if(allowedState==null){ //Newclass--rememberwhetheritisallowed clazz=mContext.getClassLoader().loadClass( prefix!=null?(prefix+name):name).asSubclass(View.class); booleanallowed=clazz!=null&&mFilter.onLoadClass(clazz); mFilterMap.put(name,allowed); if(!allowed){ failNotAllowed(name,prefix,attrs); } }elseif(allowedState.equals(Boolean.FALSE)){ failNotAllowed(name,prefix,attrs); } } } Object[]args=mConstructorArgs; args[1]=attrs; finalViewview=constructor.newInstance(args); if(viewinstanceofViewStub){ //alwaysuseourselveswheninflatingViewStublater finalViewStubviewStub=(ViewStub)view; viewStub.setLayoutInflater(this); } returnview; }catch(NoSuchMethodExceptione){ InflateExceptionie=newInflateException(attrs.getPositionDescription() +":Errorinflatingclass" +(prefix!=null?(prefix+name):name)); ie.initCause(e); throwie; }catch(ClassCastExceptione){ //IfloadedclassisnotaViewsubclass InflateExceptionie=newInflateException(attrs.getPositionDescription() +":ClassisnotaView" +(prefix!=null?(prefix+name):name)); ie.initCause(e); throwie; }catch(ClassNotFoundExceptione){ //IfloadClassfails,weshouldpropagatetheexception. throwe; }catch(Exceptione){ InflateExceptionie=newInflateException(attrs.getPositionDescription() +":Errorinflatingclass" +(clazz==null?"<unknown>":clazz.getName())); ie.initCause(e); throwie; } }
从onCreateView(...)中我们知道,其实createViewFromTag(...)中对View的初始化最终都是通过createView(...)这个函数进行初始化的,不同只在于系统控件需要通过onCreateView(...)加上前缀,以便类加载器(ClassLoader)正确地通过类所在的包初始化这个类。createView(...)这个函数的思路很清晰,不看catch里面的内容,try里面开头的两个分支就是用来将所要用的类构造函数提取出来,Android系统会对使用过的类构造函数进行缓存,因为像TextView这些常用的控件可能会被使用很多次。接下来,就是通过类构造函数对View进行初始化了。我们注意到传入构造函数的mConstructorArgs是一个包含两个元素的数组。
finalObject[]mConstructorArgs=newObject[2];
那么我们就很清楚了,它就是调用系统控件中对应两个参数的构造函数。为了方便,我们就从最基础的View进行分析。
publicView(Contextcontext,AttributeSetattrs){ this(context,attrs,0); } publicView(Contextcontext,AttributeSetattrs,intdefStyle){ this(context); TypedArraya=context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.View, defStyle,0); Drawablebackground=null; intleftPadding=-1; inttopPadding=-1; intrightPadding=-1; intbottomPadding=-1; intstartPadding=UNDEFINED_PADDING; intendPadding=UNDEFINED_PADDING; intpadding=-1; intviewFlagValues=0; intviewFlagMasks=0; booleansetScrollContainer=false; intx=0; inty=0; floattx=0; floatty=0; floatrotation=0; floatrotationX=0; floatrotationY=0; floatsx=1f; floatsy=1f; booleantransformSet=false; intscrollbarStyle=SCROLLBARS_INSIDE_OVERLAY; intoverScrollMode=mOverScrollMode; booleaninitializeScrollbars=false; booleanleftPaddingDefined=false; booleanrightPaddingDefined=false; booleanstartPaddingDefined=false; booleanendPaddingDefined=false; finalinttargetSdkVersion=context.getApplicationInfo().targetSdkVersion; finalintN=a.getIndexCount(); for(inti=0;i<N;i++){ intattr=a.getIndex(i); switch(attr){ casecom.android.internal.R.styleable.View_background: background=a.getDrawable(attr); break; casecom.android.internal.R.styleable.View_padding: padding=a.getDimensionPixelSize(attr,-1); mUserPaddingLeftInitial=padding; mUserPaddingRightInitial=padding; leftPaddingDefined=true; rightPaddingDefined=true; break; //省略一大串无关的函数 }
由于我们只关注View中的背景图是怎么加载的,注意这个函数其实就是遍历AttributeSetattrs这个东西,然后对View的各个属性进行初始化。我们直接进入
background=a.getDrawable(attr);
这里看看(TypedArray.getDrawable)。
publicDrawablegetDrawable(intindex){ finalTypedValuevalue=mValue; if(getValueAt(index*AssetManager.STYLE_NUM_ENTRIES,value)){ if(false){ System.out.println("******************************************************************"); System.out.println("Gotdrawableresource:type=" +value.type +"str="+value.string +"int=0x"+Integer.toHexString(value.data) +"cookie="+value.assetCookie); System.out.println("******************************************************************"); } returnmResources.loadDrawable(value,value.resourceId); } returnnull; }
我们发现它调用mResources.loadDrawable(...),进去看看。
/*package*/DrawableloadDrawable(TypedValuevalue,intid) throwsNotFoundException{ if(TRACE_FOR_PRELOAD){ //Logonlyframeworkresources if((id>>>24)==0x1){ finalStringname=getResourceName(id); if(name!=null)android.util.Log.d("PreloadDrawable",name); } } booleanisColorDrawable=false; if(value.type>=TypedValue.TYPE_FIRST_COLOR_INT&& value.type<=TypedValue.TYPE_LAST_COLOR_INT){ isColorDrawable=true; } finallongkey=isColorDrawable?value.data: (((long)value.assetCookie)<<32)|value.data; Drawabledr=getCachedDrawable(isColorDrawable?mColorDrawableCache:mDrawableCache,key); if(dr!=null){ returndr; } Drawable.ConstantStatecs=isColorDrawable ?sPreloadedColorDrawables.get(key) :(sPreloadedDensity==mConfiguration.densityDpi ?sPreloadedDrawables.get(key):null); if(cs!=null){ dr=cs.newDrawable(this); }else{ if(isColorDrawable){ dr=newColorDrawable(value.data); } if(dr==null){ if(value.string==null){ thrownewNotFoundException( "ResourceisnotaDrawable(colororpath):"+value); } Stringfile=value.string.toString(); if(TRACE_FOR_MISS_PRELOAD){ //Logonlyframeworkresources if((id>>>24)==0x1){ finalStringname=getResourceName(id); if(name!=null)android.util.Log.d(TAG,"Loadingframeworkdrawable#" +Integer.toHexString(id)+":"+name +"at"+file); } } if(DEBUG_LOAD)Log.v(TAG,"Loadingdrawableforcookie" +value.assetCookie+":"+file); if(file.endsWith(".xml")){ try{ XmlResourceParserrp=loadXmlResourceParser( file,id,value.assetCookie,"drawable"); dr=Drawable.createFromXml(this,rp); rp.close(); }catch(Exceptione){ NotFoundExceptionrnf=newNotFoundException( "File"+file+"fromdrawableresourceID#0x" +Integer.toHexString(id)); rnf.initCause(e); throwrnf; } }else{ try{ InputStreamis=mAssets.openNonAsset( value.assetCookie,file,AssetManager.ACCESS_STREAMING); // System.out.println("Openedfile"+file+":"+is); dr=Drawable.createFromResourceStream(this,value,is, file,null); is.close(); // System.out.println("Createdstream:"+dr); }catch(Exceptione){ NotFoundExceptionrnf=newNotFoundException( "File"+file+"fromdrawableresourceID#0x" +Integer.toHexString(id)); rnf.initCause(e); throwrnf; } } } } 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,"Savingcacheddrawable@#"+ // Integer.toHexString(key.intValue()) // +"in"+this+":"+cs); if(isColorDrawable){ mColorDrawableCache.put(key,newWeakReference<Drawable.ConstantState>(cs)); }else{ mDrawableCache.put(key,newWeakReference<Drawable.ConstantState>(cs)); } } } } } returndr; }
就是这个函数了,所有View的背景的加载都在这里了。这个函数的逻辑就比较复杂了,大体说来就是根据背景的类型(纯颜色、定义在XML文件中的,或者是一张静态的背景),如果缓存里面有,就直接用缓存里的。
总结一下,经过上面的分析,我们知道了,Android就是在Activity.setContentView(...)中为我们进行资源文件的加载,精确到具体的函数的话,资源文件的加载就是在每一个被初始化的View的构造函数中进行加载的。
以上就是本文的全部内容了,希望对大家能够有所帮助。