首页 > 代码库 > 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 自定义布局 性能问题 初探