关于Android UI绘制优化你应该了解的知识点
Posted bug樱樱
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于Android UI绘制优化你应该了解的知识点相关的知识,希望对你有一定的参考价值。
一、android绘制原理及工具选择
1.1、Android绘制原理
对于Android手机来说,它的画面渲染依赖于两个硬件:1.CPU;2.GPU:
- CPU负责计算显示内容,比如:视图创建、布局计算、图片解码、文本绘制等
- GPU负责栅格化(UI元素绘制到屏幕上),栅格化:将一些组件比如Button、Bitmap拆分成不同的像素进行显示然后完成绘制,这个操作相对比较耗时,所以引入GPU来加快栅格化操作
- 16ms发出VSync信号触发UI渲染,意思就是Android系统要求每一帧都要在16ms内完成,具体到项目中就是不管业务代码或者其他逻辑代码有多复杂,想要保证每一帧都很平滑,渲染代码就应该在16ms内完成
- 大多数的Android设备屏幕刷新频率:60Hz ,60帧/秒是人眼和大脑之间协作的极限
1.2、优化工具
1.Systrace
- 关注Frames
- 正常:绿色圆点,丢帧:黄色或红色
- Alerts:Systrace中自动分析并且标注异常性能的条目
上面这张图是我找的一个使用Systrace生成的.html文件,图中每一个F的出现就表明出现了一帧,可以看到这两个F之间的时间间隔比16ms多了不少,Alert type这里面就是Systrace自动给出的一些提示信息,我们可以根据提示信息来查找修改的方向。
②、Layout Inspector
菜单栏——>Tools——>Layout Inspector
- Android Studio自带的工具
- 查看试图层次结构
③、Choreographer
获取FPS,线上使用,具备实时性
- Api 16之后
- 使用方式是:Choreographer.getInstance().postFrameCallback
这里写了一个方法getFPS()
来获取这个APP的FPS情况,方法内部一开始是做了一个保护性操作,确保使用的Choreographer
发生在API16之后,然后在doFrame
回调中首先判断是不是统计周期的第一次,如果是就记录第一次回调的时间,接下来就是判断时间间隔是否超过预设的阀值160ms
,如果超过则计算FPS,计算方式是间隔时间除以间隔时间内发生的次数,如果没有超过则直接将次数加1。
输出的结果可以看到基本上都是59和60之间的数值。
二、Android布局加载原理
2.1、布局加载流程
1.源码解析
这一部分我们来看下源码,因为内容比较多,我就尽可能的简单说,对于源码阅读的流程我们之前已经说过几次了,这里就不再介绍了,基本上就是找到你需要的入口方法,然后一路跟踪下去,把整个流程串起来,不需要你把每一行的代码都读懂。
既然说的是布局加载,那么我们首先肯定是找入口方法,这个方法你回想一下每个页面加载布局都是调用的什么方法呢?很简单啦:
setContentView(R.layout.activity_main);
然后点击这个方法进入源码中去就到了AppCompatActivity类的setContentView()方法中:
@Override
public void setContentView(@LayoutRes int layoutResID)
getDelegate().setContentView(layoutResID);
继续跟踪点击setContentView()方法:
发现这是一个抽象方法,此时你需要去找它的实现类AppCompatDelegateImpl中的方法了,点击左侧向下的fx向下箭头:
@Override
public void setContentView(int resId)
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
这个方法中由于传递进来的resId也就是布局文件的id,它只在LayoutInflater这一行用到了,所以接着跟踪这一行,点击inflate()方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
return inflate(resource, root, root != null);
这个方法内部又调用了另一个inflate()方法,所以继续点击:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
final Resources res = getContext().getResources();
if (DEBUG)
Log.d(TAG, "INFLATING from resource: \\"" + res.getResourceName(resource) + "\\" ("
+ Integer.toHexString(resource) + ")");
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null)
return view;
XmlResourceParser parser = res.getLayout(resource);
try
return inflate(parser, root, attachToRoot);
finally
parser.close();
这里面又有一个inflate()
方法,入参有一个parser
,看了看上下的代码,知道了它其实是XmlResourceParser的实例,那我们先不去看这个inflate()方法具体的实现,先来看下这个parser究竟是什么?找到res.getLayout()
方法,里面传入了我们的资源id,返回的是XmlResourceParser
,看名字XML资源解析器,就知道这玩意应该很屌,来吧,继续点击getLayout()
:
@NonNull
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException
return loadXmlResourceParser(id, "layout");
没啥实质性的内容,继续点击它的实现方法loadXmlResourceParser()
:
@NonNull
@UnsupportedAppUsage
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException
final TypedValue value = obtainTempTypedValue();
try
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING)
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
finally
releaseTempTypedValue(value);
这个方法开始是一些对象的声明,后面是异常的处理,所以看下来真正有用的就是if判断里面的,它判断了value.type如果是String类型的,然后继续调用了impl的loadXmlResourceParser()方法,我们点进去看下:
/**
* Loads an XML parser for the specified file.
*
* @param file the path for the XML file to parse
* @param id the resource identifier for the file
* @param assetCookie the asset cookie for the file
* @param type the type of resource (used for logging)
* @return a parser for the specified XML file
* @throws NotFoundException if the file could not be loaded
*/
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException
...
//代码有点多就不贴了,不然文章会很长,大家有需要的自己对照这个过程读一下源码,敬请谅解
主要看注释那里的说明哈,Android中的布局都是写在XML文件中的,这个方法就是为我们具体所写的布局文件准备一个XML的解析器,所以它实际上就是一个XML的Pull解析的过程。需要注意的是:android的布局实际上是一个XML文件,它在加载的时候会首先将它读取到内存中,这个过程实际上就是一个IO过程,一般在android开发中操作IO都会将其置于工作线程中,所以这里可能会成为我们优化的一个方向。
关于这个XmlResourceParser就说到这里,下面继续回到上面说的那个inflate()方法中:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)
synchronized (mConstructorArgs)
。。。
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, inflaterContext, attrs, false);
else
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
。。。
。。。
return result;
这里同样的省略了部分代码,我们知道日常开发中经常会碰到一些报错,其实这些报错在Android的源码中都是有所体现的,比如这里定义的关于merge标签的一个异常信息。接着看createViewFromTag()
这个方法,看名字我们应该能大致猜测出来它是干嘛的了,它应该就是通过一系列的Tag来创建相对应的View,我们点击该方法跟进:
@UnsupportedAppUsage
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs)
return createViewFromTag(parent, name, context, attrs, false);
这里面又调用了另一个createViewFromTag()
方法,继续跟进:
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr)
if (name.equals("view"))
name = attrs.getAttributeValue(null, "class");
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr)
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0)
context = new ContextThemeWrapper(context, themeResId);
ta.recycle();
try
View view = tryCreateView(parent, name, context, attrs);
if (view == null)
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try
if (-1 == name.indexOf('.'))
view = onCreateView(context, parent, name, attrs);
else
view = createView(context, name, null, attrs);
finally
mConstructorArgs[0] = lastContext;
return view;
catch (InflateException e)
throw e;
catch (ClassNotFoundException e)
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
catch (Exception e)
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
这里就到了重点的地方了,这里面就是创建View的过程了:
首先:View view = tryCreateView(parent, name, context, attrs); 它通过这个tryCreateView()方法构建出View对象,进到这个方法中:
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs)
if (name.equals(TAG_1995))
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
View view;
if (mFactory2 != null)
view = mFactory2.onCreateView(parent, name, context, attrs);
else if (mFactory != null)
view = mFactory.onCreateView(name, context, attrs);
else
view = null;
if (view == null && mPrivateFactory != null)
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
return view;
这个方法里面就是判断了几个factory是否为空
首先是Factory2,如果Factory2不为空则调用Factory2的onCreateView()方法创建View对象,否则判断Factory是否为空,如果Factory不为空则调用Factory的onCreateView()创建View对象,如果都为空,则View为空。
如果view为空并且PrivateFactory不为空,则调用PrivateFactory
的onCreateView()方法构建View,需要注意的是PrivateFactory它只用于Fragment标签的加载。当这些条件都不满足的时候,我们回到上面的createViewFromTag()方法中接着看,它会走到view==null
的条件判断中去,它会走onCreateView()或者createView(),点击createView()
继续跟踪:
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor))
constructor = null;
sConstructorMap.remove(name);
Class<? extends View> clazz = null;
try
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null)
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null)
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed)
failNotAllowed(name, prefix, viewContext, attrs);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
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 = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed)
failNotAllowed(name, prefix, viewContext, attrs);
else if (allowedState.equals(Boolean.FALSE))
failNotAllowed(name, prefix, viewContext, attrs);
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try
final View view = constructor.newInstance(args);
if (view instanceof ViewStub)
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
return view;
finally
mConstructorArgs[0] = lastContext;
。。。
这个方法里面constructor = clazz.getConstructor(mConstructorSignature)
; constructor.setAccessible(true)
; 这两行首先找到clazz的构造方法,通过反射的方式将其设置为外部可调用的,然后下面final View view = constructor.newInstance(args); 这一行它通过构造函数反射创建了View,在这个方法中是真正进行了View的创建,当然这是在没有使用Factory的情况下哦。这个过程实际上它是使用了反射,反射是有可能导致程序变慢的一个因素,所以这里也可以作为我们的一个优化点。
2.布局加载流程总结
2.2、性能瓶颈
- 布局文件解析:IO过程(文件过大时可能会导致卡顿)
- 创建View对象:反射(使用过多也会导致变慢)
2.3、LayoutInflater.Factory
在上面解读setContentView的源码时,我们知道创建View的过程优先是使用Factory2和Factory进行创建,下面对这两个类作简要说明:
LayoutInflater.Factory:
- LayoutInflater创建View的一个Hook,Hook其实就是我们可以将自己的代码挂在它的原始代码之上,可以对它的流程进行更改
- 定制创建View的过程:比如全局替换自定义TextView等
Factory与Factory2
- Factory2继承于Factory
- 多了一个参数:parent
我们来看一下它们的源码,首先来看Factory2:
public interface Factory2 extends Factory
@Nullable
View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs);
可以看到Factory2是一个接口,并且它是继承自Factory的,来看一下Factory:
public interface Factory
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
@Nullable
View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs);
入参中有个name,来看一下它的注释,意思就是我们要加载的Tag,比如这个Tag是TextView,那么通过这个方法返回的就是TextView,实际上如果你继续跟踪的话,你会发现这个Tag实际上就是我们平时在布局中写的一个个的控件:比如TextView、ImageView等等,它会根据具体的Tag来进行对应View的创建:
switch (name)
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
case "ToggleButton":
view = createToggleButton(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
并且我们对比两个接口,可以发现Factory2比Factory就是入参多了一个parent,这个parent就是你创建的View的parent,所以综上可得Factory2比Factory功能上更加强大。
三、优雅获取界面布局耗时
随着项目的不断升级,项目体量逐渐变大,页面可能也变的越来越多,然后我们希望能够在线上进行统计,了解到具体哪些页面用户在进入时会出现卡顿,布局文件加载也可能会导致卡顿。
常规方式:覆写方法(setContentView)、手动埋点上报服务端(不够优雅,代码具有侵入性)
AOP方式:切Activity的setContentView(切面点)
@ Around(“execution(*android.app.Activity.setContentView(…))”)
具体实现:
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint)
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try
joinPoint.proceed();
catch (Throwable throwable)
throwable.printStackTrace();
Log.i(name, " cost " + (System.currentTimeMillis() - time));
结果如下:
思考:如何获取每一个控件加载耗时?
我们在上面使用setContentView获取到的是页面中所有控件的耗时情况,那现在我想要知道这个页面中各个控件的耗时分布情况,以便于整体的把控分析并且可以对耗时较多的控件做针对性的优化,这样一个场景该如何实现呢?由于每个页面布局中的控件都是不可控的,有可能多也有可能少,所以我们应该尽量做到低侵入性,这个问题大家可以好好想想,看看有什么解决方案。
解决方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的兼容类)让它在创建View时进行Hook:
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2()
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs)
long time = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
Log.i(name,"控件耗时:" + (System.currentTimeMillis() - time));
return view;
@Override
public View onCreateView(String name, Context context, AttributeSet attrs)
return null;
);
结果如下:可以看到我们确实获取到了列表Item中的每个控件的耗时情况
四、异步Inflate实战
在上面我们已经说过了布局文件加载慢主要的原因是有以下两点:
- 布局文件读取慢:IO过程
- 创建View慢:通过反射创建一个对象比直接new一个对象要慢3倍,布局嵌套层级复杂则反射更多
针对上面说的这两种情况,相对应的解决套路也就是两种:
- 根本性解决:去掉IO过程、不使用反射
- 侧面缓解:让主线程不耗时,不影响主线程
这里针对侧面缓解的方案来介绍一种实现方式:AsyncLayoutInflater,谷歌提供的一个类,简称异步Inflate
- WorkThread加载布局,原生是在UI Thread加载布局
- 加载完成之后回调主线程,此时主线程拿到的是创建完成的View对象可以直接使用
- 节约主线程时间,因为耗时是发生在了异步线程中,主线程的响应能够得到保障
使用方式:首先导入asynclayoutinflater的依赖库,这里我们参考谷歌官方文档中androidx的使用:
然后来修改我们的MainActivity中的onCreate()
方法:
@Override
protected void onCreate(Bundle savedInstanceState)
new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener()
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent)
setContentView(view);
mRecycler = findViewById(R.id.mRecycler);
mRecycler.setLayoutManager(new LinearLayoutManager(MainActivity.this));
mRecycler.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL));
mRecycler.setAdapter(mAdapter);
mAdapter.setOnFeedShowCallBack(MainActivity.this);
);
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
mAdapter = new FeedAdapter(this, mList);
initData();
// getFPS();
有兴趣的可以去看一下AsyncLayoutInflater的源码,理解起来应该不难,这个类内部有一个Handler对象,一个InflateThread类继承于Thread,还有一个inflate
方法,该方法有三个入参resid、parent、callback,同时将这三个参数封装成了InflateRequest的数据结构,然后加到线程的队列中,线程中同时有一个run()方法在不断执行,它会从队列中取出一条InflateRequest,然后这个request.inflate开始执行inflate()方法并返回request.view,这个方法是执行在子线程中的,最后通过Handler将它回调到主线程中,同时有一个相关联的Callback,在Callback中进行判断如果没有创建完成的话,会回退到主线程中进行布局的加载,最后将request.view回调到onInflateFinished()方法中,这样主线程就可以在该方法中拿到对应的view了。
总结:
- 不能设置LayoutInflater.Factory(),需要自定义AsyncLayoutInflater解决;
- 注意View中不能有依赖主线程的操作
五、X2C框架使用
上面这一部分是介绍了一种侧面缓解的方式,那这一部分我们来思考一下从根本上解决该如何实现?
首先来说一下思路哈,其实也没啥思路,就是利用Java代码写布局,这种方案的特点如下:
- 本质上解决了性能问题(没有xml文件也就没有了IO的过程,直接new对象没有了反射的过程)
- 引入新问题:不便于开发、可维护性差
思路有了但是看着实现起来却不太现实哈,那咋办呢?咋办呢?咋办呢?嗯,这样拌,大神还是很多的,我们使用开源方案X2C:
X2C框架介绍:保留XML优点,解决其性能问题
- 开发人员写XML,加载Java代码
- 原理:APT编译期翻译XML为Java代码
X2C框架的使用方式:
- 添加依赖:app/build.gradle中添加
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
- 添加注解:在使用布局的任意java类或方法上面添加:
@Xml(layouts = "activity_main")
- 代码实战
将原有的setContentView注释掉,然后使用X2C.setContentView()来设置布局,运行之后发现是可以正常加载的,图中左侧圈出来的是使用X2C编译之后的产物,这个其实就是它的底层实现原理了,我们来看一下:
首先是布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
然后是编译之后的代码:
public class X2C0_Activity_Main implements IViewCreator
@Override
public View createView(Context ctx)
Resources res = ctx.getResources();
LinearLayout linearLayout0 = new LinearLayout(ctx);
linearLayout0.setOrientation(LinearLayout.VERTICAL);
RecyclerView recyclerView1 = new RecyclerView(ctx);
LinearLayout.LayoutParams layoutParam1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
recyclerView1.setId(R.id.mRecycler);
recyclerView1.setLayoutParams(layoutParam1);
linearLayout0.addView(recyclerView1);
return linearLayout0;
可以看到它内部就是将我们布局文件中的控件全都以Java对象的形式给new出来了。
X2C存在的问题:
- XML中有的部分属性Java不支持(虽然不多但是也有)
- 失去了系统的兼容(AppCompat,如果你需要使用AppCompatXXX下面的控件可以通过修改X2C源码来定制化实现相关功能)
六、视图绘制优化
1.视图绘制流程
- 测量:确定大小(自顶向下进行视图树的遍历,确定ViewGroup和View应该有多大)
- 布局:确定位置(执行另一个自顶向下的遍历操作,ViewGroup会根据测量阶段测定的大小确定自己应该摆放的位置)
- 绘制:绘制视图(对于视图树中的每个对象系统都会为它创建一个Canvas对象,然后向GPU发送一条绘制命令进行绘制)
可能存在的性能问题:
- 每个阶段耗时
- 自顶而下的遍历(如果Layout层级比较深则遍历也是很耗时的)
- 触发多次(比如嵌套使用RelativeLayout有可能会导致绘制环节触发多次)
2.布局层级及复杂度
编写布局的准则:减少View树层级
- 不嵌套使用RelativeLayout
- 不在嵌套的LinearLayout中使用weight
- merge标签:减少一个层级,只能用于根View
这里推荐使用:ConstraintLayout,网上关于它有很多的文章,后面我也准备专门写一篇它的使用总结
- 实现几乎完全扁平化布局
- 构建复杂布局性能更高
- 具有RelativeLayout和LinearLayout特性
3.过度绘制
- 一个像素最好只被绘制一次
- 调试GPU过度绘制
- 蓝色可接受
避免过度绘制方法:
- 去掉多余背景色,减少复杂shape使用
- 避免层级叠加
- 自定义View使用clipRect屏蔽被遮盖View绘制(当覆写onDraw()之后,系统就无法知道View中各个元素的位置和层级关系,就无法做自动优化,即无法自动忽略绘制那些不可见的元素)
4.布局绘制的其它优化技巧
- ViewStub:高效占位符、延迟初始化(这个标签没有大小,也没有绘制功能不参与measure和layout过程,资源消耗非常低,一般用于延迟初始化)
- onDraw中避免:创建大对象、耗时操作
- TextView相关优化(setText显示静态文本)
作者:小尘
链接:https://juejin.cn/post/7126829631583289357
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
一、架构师筑基必备技能
1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……
二、Android百大框架源码解析
1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程
三、Android性能优化实战解析
- 腾讯Bugly:对字符串匹配算法的一点理解
- 爱奇艺:安卓APP崩溃捕获方案——xCrash
- 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
- 百度APP技术:Android H5首屏优化实践
- 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
- 携程:从智行 Android 项目看组件化架构实践
- 网易新闻构建优化:如何让你的构建速度“势如闪电”?
- …
四、高级kotlin强化实战
1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》
-
从一个膜拜大神的 Demo 开始
-
Kotlin 写 Gradle 脚本是一种什么体验?
-
Kotlin 编程的三重境界
-
Kotlin 高阶函数
-
Kotlin 泛型
-
Kotlin 扩展
-
Kotlin 委托
-
协程“不为人知”的调试技巧
-
图解协程:suspend
五、Android高级UI开源框架进阶解密
1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
六、NDK模块开发
1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习
七、Flutter技术进阶
1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)
…
八、微信小程序开发
1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓
以上是关于关于Android UI绘制优化你应该了解的知识点的主要内容,如果未能解决你的问题,请参考以下文章