首页 > 代码库 > Android 自定义布局 性能问题 初探

Android 自定义布局 性能问题 初探

大家在写android 代码的时候,基本上都使用过如下几种布局 RelativeLayout,LinearLayout, FrameLayout

但是很多时候 这几种布局 也无法满足我们的使用。于是我们会考虑用自定义布局,使用自定义布局会有几个优点

比如可以减少view的使用啊,让ui显示的更加有效率啊,以及实现一些原生控件无法实现的效果。

 

我们首先去github上 下载一个开源项目 https://github.com/lucasr/android-layout-samples

注意这个项目是基于android studio 结构的。你如果用Eclipse来导入是导入不成功的。

最近github上很多开源项目都开始支持android studio了。所以还是建议大家拥抱下谷歌的新ide。

 

然后这个项目的作者是http://lucasr.org/about/ 就是国外一个很牛逼的android 工程师,我们就以他

的开源项目以及博客 来感受一下 自定义布局的性能。

 

这个项目运行起来以后实际上就是仿照的twitter的一些效果。图片库用的是picasso。有兴趣的同学可以

去http://square.github.io/picasso/  这个地方看一下这个图片库。

然后我们来看第一个自定义ui  TweetCompositeView

 1 /* 2  * Copyright (C) 2014 Lucas Rocha 3  * 4  * Licensed under the Apache License, Version 2.0 (the "License"); 5  * you may not use this file except in compliance with the License. 6  * You may obtain a copy of the License at 7  * 8  *     http://www.apache.org/licenses/LICENSE-2.0 9  *10  * Unless required by applicable law or agreed to in writing, software11  * distributed under the License is distributed on an "AS IS" BASIS,12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13  * See the License for the specific language governing permissions and14  * limitations under the License.15  */16 17 package org.lucasr.layoutsamples.widget;18 19 import android.content.Context;20 import android.text.TextUtils;21 import android.util.AttributeSet;22 import android.view.LayoutInflater;23 import android.view.View;24 import android.widget.ImageView;25 import android.widget.RelativeLayout;26 import android.widget.TextView;27 28 import org.lucasr.layoutsamples.adapter.Tweet;29 import org.lucasr.layoutsamples.adapter.TweetPresenter;30 import org.lucasr.layoutsamples.app.R;31 import org.lucasr.layoutsamples.util.ImageUtils;32 33 import java.util.EnumMap;34 import java.util.EnumSet;35 36 public class TweetCompositeView extends RelativeLayout implements TweetPresenter {37     private final ImageView mProfileImage;38     private final TextView mAuthorText;39     private final TextView mMessageText;40     private final ImageView mPostImage;41     private final EnumMap<Action, ImageView> mActionIcons;42 43     public TweetCompositeView(Context context, AttributeSet attrs) {44         this(context, attrs, 0);45     }46 47     public TweetCompositeView(Context context, AttributeSet attrs, int defStyleAttr) {48         super(context, attrs, defStyleAttr);49 50         LayoutInflater.from(context).inflate(R.layout.tweet_composite_view, this, true);51         mProfileImage = (ImageView) findViewById(R.id.profile_image);52         mAuthorText = (TextView) findViewById(R.id.author_text);53         mMessageText = (TextView) findViewById(R.id.message_text);54         mPostImage = (ImageView) findViewById(R.id.post_image);55 56         mActionIcons = new EnumMap(Action.class);57         for (Action action : Action.values()) {58             final ImageView icon;59             switch (action) {60                 case REPLY:61                     icon = (ImageView) findViewById(R.id.reply_action);62                     break;63 64                 case RETWEET:65                     icon = (ImageView) findViewById(R.id.retweet_action);66                     break;67 68                 case FAVOURITE:69                     icon = (ImageView) findViewById(R.id.favourite_action);70                     break;71 72                 default:73                     throw new IllegalArgumentException("Unrecognized tweet action");74             }75 76             mActionIcons.put(action, icon);77         }78     }79 80     @Override81     public boolean shouldDelayChildPressedState() {82         return false;83     }84 85     @Override86     public void update(Tweet tweet, EnumSet<UpdateFlags> flags) {87         mAuthorText.setText(tweet.getAuthorName());88         mMessageText.setText(tweet.getMessage());89 90         final Context context = getContext();91         ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags);92 93         final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl());94         mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE);95         if (hasPostImage) {96             ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags);97         }98     }99 }

 

我们可以看一下这个自定义ui。实际上这个自定义ui非常简单,我们工作中也经常这样使用自定义ui。

他一般就是这么使用的:

1 继承一个layout。当然这个layout可以是相对布局 也可以是流布局 

2 在构造函数里 inflate 我们的布局文件 同时初始化我们的自定义布局的子元素

3 增加一些对应的方法 来更新我们的元素 比如说 update 这个方法 就是来做这个工作的。

 

然后我们来看一下这个布局对应的布局文件

 

 1 <?xml version="1.0" encoding="utf-8"?> 2 <!-- 3   ~ Copyright (C) 2014 Lucas Rocha 4   ~ 5   ~ Licensed under the Apache License, Version 2.0 (the "License"); 6   ~ you may not use this file except in compliance with the License. 7   ~ You may obtain a copy of the License at 8   ~ 9   ~     http://www.apache.org/licenses/LICENSE-2.010   ~11   ~ Unless required by applicable law or agreed to in writing, software12   ~ distributed under the License is distributed on an "AS IS" BASIS,13   ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14   ~ See the License for the specific language governing permissions and15   ~ limitations under the License.16   -->17 18 <merge xmlns:android="http://schemas.android.com/apk/res/android"19     android:layout_width="match_parent"20     android:layout_height="match_parent">21 22     <ImageView23         android:id="@+id/profile_image"24         android:layout_width="@dimen/tweet_profile_image_size"25         android:layout_height="@dimen/tweet_profile_image_size"26         android:layout_marginRight="@dimen/tweet_content_margin"27         android:scaleType="centerCrop"/>28 29     <TextView30         android:id="@+id/author_text"31         android:layout_width="fill_parent"32         android:layout_height="wrap_content"33         android:layout_toRightOf="@id/profile_image"34         android:layout_alignTop="@id/profile_image"35         android:textColor="@color/tweet_author_text_color"36         android:textSize="@dimen/tweet_author_text_size"37         android:singleLine="true"/>38 39     <TextView40         android:id="@+id/message_text"41         android:layout_width="fill_parent"42         android:layout_height="wrap_content"43         android:layout_below="@id/author_text"44         android:layout_alignLeft="@id/author_text"45         android:textColor="@color/tweet_message_text_color"46         android:textSize="@dimen/tweet_message_text_size"/>47 48     <ImageView49         android:id="@+id/post_image"50         android:layout_width="fill_parent"51         android:layout_height="@dimen/tweet_post_image_height"52         android:layout_below="@id/message_text"53         android:layout_alignLeft="@id/message_text"54         android:layout_marginTop="@dimen/tweet_content_margin"55         android:scaleType="centerCrop"/>56 57     <LinearLayout android:layout_width="fill_parent"58         android:layout_height="wrap_content"59         android:layout_below="@id/post_image"60         android:layout_alignLeft="@id/message_text"61         android:layout_marginTop="@dimen/tweet_content_margin"62         android:orientation="horizontal">63 64         <ImageView65             android:id="@+id/reply_action"66             android:layout_width="0dp"67             android:layout_height="@dimen/tweet_icon_image_size"68             android:layout_weight="1"69             android:src="http://www.mamicode.com/@drawable/tweet_reply"70             android:scaleType="fitStart"/>71 72         <ImageView73             android:id="@+id/retweet_action"74             android:layout_width="0dp"75             android:layout_height="@dimen/tweet_icon_image_size"76             android:layout_weight="1"77             android:src="http://www.mamicode.com/@drawable/tweet_retweet"78             android:scaleType="fitStart"/>79 80         <ImageView81             android:id="@+id/favourite_action"82             android:layout_width="0dp"83             android:layout_height="@dimen/tweet_icon_image_size"84             android:layout_weight="1"85             android:src="http://www.mamicode.com/@drawable/tweet_favourite"86             android:scaleType="fitStart"/>87 88     </LinearLayout>89 90 </merge>

 

我们可以来看一下这个布局 其中包含了 LinearLayout 这个布局。 我们知道在android里面 linearlayout和relativelayout 是非常复杂的ui

这种viewgroup 会不断的检测子view的大小和布局位置。 所以实际上效率是有损失的。所以我们如果想更近一步的 优化我们的ui效率

我们要尽量避免使用这种高级的viewgroup 

比如我们可以来看看这个view

  1 /*  2  * Copyright (C) 2014 Lucas Rocha  3  *  4  * Licensed under the Apache License, Version 2.0 (the "License");  5  * you may not use this file except in compliance with the License.  6  * You may obtain a copy of the License at  7  *  8  *     http://www.apache.org/licenses/LICENSE-2.0  9  * 10  * Unless required by applicable law or agreed to in writing, software 11  * distributed under the License is distributed on an "AS IS" BASIS, 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13  * See the License for the specific language governing permissions and 14  * limitations under the License. 15  */ 16  17 package org.lucasr.layoutsamples.widget; 18  19 import android.content.Context; 20 import android.text.TextUtils; 21 import android.util.AttributeSet; 22 import android.view.LayoutInflater; 23 import android.view.View; 24 import android.view.ViewGroup; 25 import android.widget.ImageView; 26 import android.widget.TextView; 27  28 import org.lucasr.layoutsamples.adapter.Tweet; 29 import org.lucasr.layoutsamples.adapter.TweetPresenter; 30 import org.lucasr.layoutsamples.app.R; 31 import org.lucasr.layoutsamples.util.ImageUtils; 32  33 import java.util.EnumMap; 34 import java.util.EnumSet; 35  36 public class TweetLayoutView extends ViewGroup implements TweetPresenter { 37     private final ImageView mProfileImage; 38     private final TextView mAuthorText; 39     private final TextView mMessageText; 40     private final ImageView mPostImage; 41     private final EnumMap<Action, View> mActionIcons; 42  43     public TweetLayoutView(Context context, AttributeSet attrs) { 44         this(context, attrs, 0); 45     } 46  47     public TweetLayoutView(Context context, AttributeSet attrs, int defStyleAttr) { 48         super(context, attrs, defStyleAttr); 49  50         LayoutInflater.from(context).inflate(R.layout.tweet_layout_view, this, true); 51         mProfileImage = (ImageView) findViewById(R.id.profile_image); 52         mAuthorText = (TextView) findViewById(R.id.author_text); 53         mMessageText = (TextView) findViewById(R.id.message_text); 54         mPostImage = (ImageView) findViewById(R.id.post_image); 55  56         mActionIcons = new EnumMap(Action.class); 57         for (Action action : Action.values()) { 58             final int viewId; 59             switch (action) { 60                 case REPLY: 61                     viewId = R.id.reply_action; 62                     break; 63  64                 case RETWEET: 65                     viewId = R.id.retweet_action; 66                     break; 67  68                 case FAVOURITE: 69                     viewId = R.id.favourite_action; 70                     break; 71  72                 default: 73                     throw new IllegalArgumentException("Unrecognized tweet action"); 74             } 75  76             mActionIcons.put(action, findViewById(viewId)); 77         } 78     } 79  80     private void layoutView(View view, int left, int top, int width, int height) { 81         MarginLayoutParams margins = (MarginLayoutParams) view.getLayoutParams(); 82         final int leftWithMargins = left + margins.leftMargin; 83         final int topWithMargins = top + margins.topMargin; 84  85         view.layout(leftWithMargins, topWithMargins, 86                     leftWithMargins + width, topWithMargins + height); 87     } 88  89     private int getWidthWithMargins(View child) { 90         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 91         return child.getWidth() + lp.leftMargin + lp.rightMargin; 92     } 93  94     private int getHeightWithMargins(View child) { 95         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); 96         return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; 97     } 98  99     private int getMeasuredWidthWithMargins(View child) {100         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();101         return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;102     }103 104     private int getMeasuredHeightWithMargins(View child) {105         final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();106         return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;107     }108 109     @Override110     public boolean shouldDelayChildPressedState() {111         return false;112     }113 114     @Override115     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {116         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);117 118         int widthUsed = 0;119         int heightUsed = 0;120 121         measureChildWithMargins(mProfileImage,122                                 widthMeasureSpec, widthUsed,123                                 heightMeasureSpec, heightUsed);124         widthUsed += getMeasuredWidthWithMargins(mProfileImage);125 126         measureChildWithMargins(mAuthorText,127                                 widthMeasureSpec, widthUsed,128                                 heightMeasureSpec, heightUsed);129         heightUsed += getMeasuredHeightWithMargins(mAuthorText);130 131         measureChildWithMargins(mMessageText,132                                 widthMeasureSpec, widthUsed,133                                 heightMeasureSpec, heightUsed);134         heightUsed += getMeasuredHeightWithMargins(mMessageText);135 136         if (mPostImage.getVisibility() != View.GONE) {137             measureChildWithMargins(mPostImage,138                                     widthMeasureSpec, widthUsed,139                                     heightMeasureSpec, heightUsed);140             heightUsed += getMeasuredHeightWithMargins(mPostImage);141         }142 143         int maxIconHeight = 0;144         for (Action action : Action.values()) {145             final View iconView = mActionIcons.get(action);146             measureChildWithMargins(iconView,147                                     widthMeasureSpec, widthUsed,148                                     heightMeasureSpec, heightUsed);149 150             final int height = getMeasuredHeightWithMargins(iconView);151             if (height > maxIconHeight) {152                 maxIconHeight = height;153             }154 155             widthUsed += getMeasuredWidthWithMargins(iconView);156         }157         heightUsed += maxIconHeight;158 159         int heightSize = heightUsed + getPaddingTop() + getPaddingBottom();160         setMeasuredDimension(widthSize, heightSize);161     }162 163     @Override164     protected void onLayout(boolean changed, int l, int t, int r, int b) {165         final int paddingLeft = getPaddingLeft();166         final int paddingTop = getPaddingTop();167 168         int currentTop = paddingTop;169 170         layoutView(mProfileImage, paddingLeft, currentTop,171                    mProfileImage.getMeasuredWidth(),172                    mProfileImage.getMeasuredHeight());173 174         final int contentLeft = getWidthWithMargins(mProfileImage) + paddingLeft;175         final int contentWidth = r - l - contentLeft - getPaddingRight();176 177         layoutView(mAuthorText, contentLeft, currentTop,178                    contentWidth, mAuthorText.getMeasuredHeight());179         currentTop += getHeightWithMargins(mAuthorText);180 181         layoutView(mMessageText, contentLeft, currentTop,182                 contentWidth, mMessageText.getMeasuredHeight());183         currentTop += getHeightWithMargins(mMessageText);184 185         if (mPostImage.getVisibility() != View.GONE) {186             layoutView(mPostImage, contentLeft, currentTop,187                        contentWidth, mPostImage.getMeasuredHeight());188 189             currentTop += getHeightWithMargins(mPostImage);190         }191 192         final int iconsWidth = contentWidth / mActionIcons.size();193         int iconsLeft = contentLeft;194 195         for (Action action : Action.values()) {196             final View icon = mActionIcons.get(action);197 198             layoutView(icon, iconsLeft, currentTop,199                        iconsWidth, icon.getMeasuredHeight());200             iconsLeft += iconsWidth;201         }202     }203 204     @Override205     public LayoutParams generateLayoutParams(AttributeSet attrs) {206         return new MarginLayoutParams(getContext(), attrs);207     }208 209     @Override210     protected LayoutParams generateDefaultLayoutParams() {211         return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);212     }213 214     @Override215     public void update(Tweet tweet, EnumSet<UpdateFlags> flags) {216         mAuthorText.setText(tweet.getAuthorName());217         mMessageText.setText(tweet.getMessage());218 219         final Context context = getContext();220         ImageUtils.loadImage(context, mProfileImage, tweet.getProfileImageUrl(), flags);221 222         final boolean hasPostImage = !TextUtils.isEmpty(tweet.getPostImageUrl());223         mPostImage.setVisibility(hasPostImage ? View.VISIBLE : View.GONE);224         if (hasPostImage) {225             ImageUtils.loadImage(context, mPostImage, tweet.getPostImageUrl(), flags);226         }227     }228 }

然后看看他的布局文件 

<?xml version="1.0" encoding="utf-8"?><!--  ~ Copyright (C) 2014 Lucas Rocha  ~  ~ Licensed under the Apache License, Version 2.0 (the "License");  ~ you may not use this file except in compliance with the License.  ~ You may obtain a copy of the License at  ~  ~     http://www.apache.org/licenses/LICENSE-2.0  ~  ~ Unless required by applicable law or agreed to in writing, software  ~ distributed under the License is distributed on an "AS IS" BASIS,  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  ~ See the License for the specific language governing permissions and  ~ limitations under the License.  --><merge xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <ImageView        android:id="@+id/profile_image"        android:layout_width="@dimen/tweet_profile_image_size"        android:layout_height="@dimen/tweet_profile_image_size"        android:layout_marginRight="@dimen/tweet_content_margin"        android:scaleType="centerCrop"/>    <TextView        android:id="@+id/author_text"        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:textColor="@color/tweet_author_text_color"        android:textSize="@dimen/tweet_author_text_size"        android:singleLine="true"/>    <TextView        android:id="@+id/message_text"        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:layout_marginBottom="@dimen/tweet_content_margin"        android:textColor="@color/tweet_message_text_color"        android:textSize="@dimen/tweet_message_text_size"/>    <ImageView        android:id="@+id/post_image"        android:layout_width="fill_parent"        android:layout_height="@dimen/tweet_post_image_height"        android:layout_marginBottom="@dimen/tweet_content_margin"        android:scaleType="centerCrop"/>    <ImageView        android:id="@+id/reply_action"        android:layout_width="@dimen/tweet_icon_image_size"        android:layout_height="@dimen/tweet_icon_image_size"        android:src="http://www.mamicode.com/@drawable/tweet_reply"        android:scaleType="fitStart"/>    <ImageView        android:id="@+id/retweet_action"        android:layout_width="@dimen/tweet_icon_image_size"        android:layout_height="@dimen/tweet_icon_image_size"        android:src="http://www.mamicode.com/@drawable/tweet_retweet"        android:scaleType="fitStart"/>    <ImageView        android:id="@+id/favourite_action"        android:layout_width="@dimen/tweet_icon_image_size"        android:layout_height="@dimen/tweet_icon_image_size"        android:src="http://www.mamicode.com/@drawable/tweet_favourite"        android:scaleType="fitStart"/></merge>

  

看一下我们就会发现,TweetLayoutView 是通过 onMeasure onlayout 自己来决定子布局的大小和位置的

完全跟linearlayout和relativelayout 没有任何关系。这样性能上就有极大的提高。

 

当然我们不可能自己实现 所有的layout对吧,不然的话 我们就都去谷歌了。。。。哈哈。

但是可以有选择的把你app里 ui最复杂的地方 选择性的优化他。提高 ui渲染的效率。

 

最后我们看一下前面这个TweetLayoutView  这个布局实际上还不是最优解。

因为里面有很多系统自带的imageview 和textview。

 

我们可以打开一下设置--开发者选项-显示布局边界 这个功能

这个功能可以把你当前app的 布局边界全部标示出来

我们可以打开android 版的gmail 随便点击个列表。 

可以看一下他们listview里的每个item 布局边界都是在外面。里面没有任何布局边界。

所以可以得知gmail的listview里的 item 是自己重写的一整个view 里面没有使用

任何系统自带的textview 或者是imageview 之类的。

这样就是ui终极进化了。。。。。。

 

当然这么做 工作量很多,而且很多地方需要考虑。比如你自己画文本是简单了,效率是提高了

但是textview 的文本截断呢?你能做么?imageview里的图片缩放呢?你能做么?

 

所以我们在自定义布局的时候 除了考虑ui实现的效率,我们还需要着重考虑实现的难度,和技术上的风险。

个人感觉只需要修改你app最卡顿的地方的布局 即可。尤其是listview viewpager里面的item

这一般在低端手机上 确实会出现卡帧的现象。其他地方看情况修改。

 

最后 https://github.com/lucasr/android-layout-samples 这个项目大家没事可以多看看,

有很多值得学习的地方。

 

Android 自定义布局 性能问题 初探