andriod开发之Activity的渲染机制

一切从setContentView说起。安卓中最常用的代码可能就是setContentView了,但大家有没有想过这个方法的背后到底做了些什么?

public class MainActivity extends Activity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
 }
}

直接跳转到Activity的源码我们可以看到,Activity.setContentView实际上调用了PhoneWindow.setContentView:

final void attach(Context context, ActivityThread aThread,
  Instrumentation instr, IBinder token, int ident,
  Application application, Intent intent, ActivityInfo info,
  CharSequence title, Activity parent, String id,
  NonConfigurationInstances lastNonConfigurationInstances,
  Configuration config, String referrer, IVoiceInteractor voiceInteractor,
  Window window) {
  ...
  mWindow = new PhoneWindow(this, window);
  ...
}

public Window getWindow() {
 return mWindow;
}

public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);
 initWindowDecorActionBar();
}

我们继续跟踪PhoneWindow的源码,可以发现最终layoutResID被inflate出来之后是成为了mDecor这个DecorView的子view。而DecorView实际上是一个FrameLayout:

public void setContentView(int layoutResID) {
  if (mContentParent == null) {
   installDecor();
  } else {
   mContentParent.removeAllViews();
  }
  mLayoutInflater.inflate(layoutResID, mContentParent);
  final Callback cb = getCallback();
  if (cb != null && !isDestroyed()) {
   cb.onContentChanged();
  }
}

private void installDecor() {
  if (mDecor == null) {
   mDecor = generateDecor();
   ...
  }
  if (mContentParent == null) {
   //mContentParent 实际上是mDecor的一个子view
   mContentParent = generateLayout(mDecor);
   ...
  }
  ...
}

protected DecorView generateDecor() {
  return new DecorView(getContext(), -1);
}

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
  ...
}

这里的generateLayout比较重要,它实际上是根据window的各种属性inflate出不同的layout挂到DecorView下面,而mContentParent是这个layout中的一个子ViewGroup。如果我们没有对window的属性进行设置就会使用默认的com.android.internal.R.layout.screen_simple这个layout:

protected ViewGroup generateLayout(DecorView decor) {
  ...
  if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
   ...
   layoutResource = com.android.internal.R.layout.screen_title_icons;
   ...
  } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
 && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
   layoutResource = com.android.internal.R.layout.screen_progress;
  } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
   ...
   layoutResource = com.android.internal.R.layout.screen_custom_title;
   ...
  } ... else{
   layoutResource = com.android.internal.R.layout.screen_simple;
  }
  ...
  View in = mLayoutInflater.inflate(layoutResource, null);
  decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
  ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
  ...
  return contentParent;
}

我们可以在AndroidSdk根目录/platforms/android-19/data/res/layout/下面找到这些layout xml,例如screen_simple,这是个竖直的LinearLayout,由上方的ActionBar和下方的content FrameLayout组成。它就是我们最常见的带ActionBar的activity样式:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:fitsSystemWindows="true"
 android:orientation="vertical">
 <ViewStub android:id="@+id/action_mode_bar_stub"
 android:inflatedId="@+id/action_mode_bar"
 android:layout="@layout/action_mode_bar"
 android:layout_width="match_parent"
 android:layout_height="wrap_content" />
 <FrameLayout
 android:id="@android:id/content"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:foregroundInsidePadding="false"
 android:foregroundGravity="fill_horizontal|top"
 android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

我们可以用一张图片来总结下Activity是如何管理布局的(这里假设DecorView里面添加了screen_simple这个布局):

Activity的布局是怎样被系统渲染的

在上一节中我们已经知道了Activity是怎样管理布局的。接着我们来看看Activity中的布局是如何渲染到系统的。

ActivityThread用于管理Activity的声明周期,之后我会专门写一篇文章来讲它。我们直接看ActivityThread.handleResumeActivity方法:

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
 ...
 //performResumeActivity方法会调用Activity.onResume
 ActivityClientRecord r = performResumeActivity(token, clearHide);
 ...
 r.window = r.activity.getWindow();
 View decor = r.window.getDecorView();
 decor.setVisibility(View.INVISIBLE);
 ViewManager wm = a.getWindowManager();
 WindowManager.LayoutParams l = r.window.getAttributes();
 a.mDecor = decor;
 l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 l.softInputMode |= forwardBit;
 if (a.mVisibleFromClient) {
 a.mWindowAdded = true;
 wm.addView(decor, l);
 }
 ...
}

可以看到它在Activity.onResume之后从Activity中获取了Window,然后又从window中获取了DecorView。最后使用WindowManager.addView将DecorView添加到了WindowManager中。这样就将DecorView在手机上渲染了出来。

WindowManager.addView方法可以将一个view渲染到手机界面上。不知道大家有没有做过类似悬浮球的应用,就是用WindowManager.addView去实现的。这里就不再展开了,大家有兴趣的话可以自己去搜索一下。

为什么不能在子线程中操作view

我们都知道,在安卓中必须在ui线程中操作ui,不能在子线程中对view进行操作,否则或抛出CalledFromWrongThreadException异常。但是在子线程中操作view是不是真的就一定会出现异常呢?让我们运行下面的代码:

public class MainActivity extends Activity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  new Thread(new Runnable() {
   @Override
   public void run() {
    ((TextView)findViewById(R.id.textView)).setText("子线程中操作view");
   }
  }).start();
 }
}

我们可以看到实际上在onCreate的时候直接启动子线程去修改TextView的文字是可以正常运行的,且文字也是显示正常的:

让我们家1秒的延迟再试一下:

public class MainActivity extends Activity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  new Thread(new Runnable() {
   @Override
   public void run() {
    try {
     Thread.sleep(1000);
    } catch (InterruptedException e) {
     e.printStackTrace();
    }
    ((TextView)findViewById(R.id.textView)).setText("子线程中操作view");
   }
  }).start();
 }
}

运行之后就能看到熟悉的崩溃日志了:

02-28 22:36:48.550 3780 3817 E AndroidRuntime: FATAL EXCEPTION: Thread-5
02-28 22:36:48.550 3780 3817 E AndroidRuntime: Process: com.example.linjw.myapplication, PID: 3780
02-28 22:36:48.550 3780 3817 E AndroidRuntime: android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6987)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1104)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.View.requestLayout(View.java:19807)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.View.requestLayout(View.java:19807)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.View.requestLayout(View.java:19807)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.View.requestLayout(View.java:19807)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.support.constraint.ConstraintLayout.requestLayout(ConstraintLayout.java:874)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.view.View.requestLayout(View.java:19807)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.widget.TextView.checkForRelayout(TextView.java:7375)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.widget.TextView.setText(TextView.java:4487)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.widget.TextView.setText(TextView.java:4344)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at android.widget.TextView.setText(TextView.java:4319)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at com.example.linjw.myapplication.MainActivity$1.run(MainActivity.java:20)
02-28 22:36:48.550 3780 3817 E AndroidRuntime:  at java.lang.Thread.run(Thread.java:760)

为什么延迟1秒之后就能看到异常被抛出了呢?本着寻根问底的精神,我们直接扣ViewRootImpl的源码看看CalledFromWrongThreadException异常是怎么被抛出的:

public ViewRootImpl(Context context, Display display) {
 ...
 mThread = Thread.currentThread();
 ...
}

void checkThread() {
 if (mThread != Thread.currentThread()) {
  throw new CalledFromWrongThreadException(
    "Only the original thread that created a view hierarchy can touch its views.");
 }
}

public void requestLayout() {
 if (!mHandlingLayoutInLayoutRequest) {
  checkThread();
  mLayoutRequested = true;
  scheduleTraversals();
 }
}

在View.requestLayout方法中会调用ViewRootImpl.requestLayout,然后在ViewRootImpl.requestLayout里面会调用ViewRootImpl.checkThread去判断当前线程和创建ViewRootImpl的线程是不是同一个线程。如果不是的话就抛出CalledFromWrongThreadException异常。

那ViewRootImpl又是在哪个线程中被创建的呢?还记得上一节中讲到的ActivityThread.handleResumeActivity方法中将DecorView添加到WindowManager中吗?WindowManager实际上是WindowManagerImpl实例:

public final class WindowManagerImpl implements WindowManager {
 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
 ...
 public void addView(View view, ViewGroup.LayoutParams params) {
  mGlobal.addView(view, params, mDisplay, mParentWindow);
 }
 ...
}

我们可以看到WindowManagerImpl.addView实际上是调到了WindowManagerGlobal.addView:

public final class WindowManagerGlobal {
 public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
 ...
 ViewRootImpl root;
 ...
 root = new ViewRootImpl(view.getContext(), display);
 ...
 }
}

所以ViewRootImpl是在handleResumeActivity的线程中被创建的,我们都知道onResume是在主线程中被调用的,所以ViewRootImpl是在主线程中被调用的。所以只要在非主线程中调用ViewRootImpl.requestLayout就会抛出CalledFromWrongThreadException异常。

那回到最初的问题,为什么我们在onCreate的时候直接起子线程去修改TextView的文字,不会抛出CalledFromWrongThreadException异常?因为ViewRootImpl是在onResume中创建的,在onCreate的时候它就还没有被创建,所以就不会抛出CalledFromWrongThreadException异常。

等到onResume的时候ViewRootImpl被创建,会进行第一次layout,这个时候才会检查是否在主线程中操作ui。

您可能感兴趣的文章:

  • Android的Activity跳转动画各种效果整理
  • android PopupWindow 和 Activity弹出窗口实现方式
  • android的activity跳转到另一个activity
  • Android基础之Fragment与Activity交互详解
  • Android Activity之间传递图片(Bitmap)的方法
  • Android笔记之:App应用之启动界面SplashActivity的使用
  • Activity透明/半透明效果的设置transparent(两种实现方法)
  • android获取当前运行Activity名字的方法
  • Android Activity切换(跳转)时出现黑屏的解决方法 分享
  • Android实现Activity界面切换添加动画特效的方法
(0)

相关推荐

  • Android基础之Fragment与Activity交互详解

    今天继续讲解Fragment组件的特性,主要是跟Activity的交互和生命周期的关系,我们前面已经说过Fragment是依赖于Activity的,而且生命周期也跟Activity绑定一起.下面我们看看Fragment跟Activity的关系. 1.为Activity创建事件回调方法在一些情况下, 你可能需要一个fragment与activity分享事件. 一个好的方法是在fragment中定义一个回调的interface, 并要求宿主activity实现它.当activity通过interfa

  • Android Activity之间传递图片(Bitmap)的方法

    在Android开发中:Activity之间传递参数是常见的事:如果我们要在Activity之间传递图片:1.MainActivity中包括一个ImageView:当我们点击ImageView时:把图片传递给另外一个Activity MainActivity的主要代码: 复制代码 代码如下: Intent intent=new Intent(MainActivity.this,TranActivity.class);            intent.putExtra("bitmap"

  • Android笔记之:App应用之启动界面SplashActivity的使用

    当前比较成熟一点的应用基本上都会在进入应用之显示一个启动界面.这个启动界面或简单,或复杂,或简陋,或华丽,用意不同,风格也不同.下面来观摩几个流行的应用的启动界面. 1. 货比三家以腾讯qq,新浪weibo,UC浏览器,游戏神庙逃亡等7个应用为例,比比看:(我认为最精美的界面应该是qq2012,虽然只有一张图,基本的应用名称,版本,图标这些信息都有,但是看着舒服,觉得美.) 2. 元素启动界面的本意是以友好用户界面来掩饰后台缓冲加载,让用户用平和等待的心情进入正常应用界面.但是因为启动界面是放在

  • android的activity跳转到另一个activity

    开发环境:android4.1.1 实验功能:在第一个Hello World!为标签的activity中显示good,该界面中有一个名为Next的按钮.点击Next按钮进入到第二个activity中去,第二个界面中只有1个Close按钮.当然,据网上有人将要比较安全的实现关闭程序的功能也不是挺简单的,因为android有专门的退出键返回键等.所以该Close按钮暂时没去实现它.我的第1个activity为HelloworldActivity,第2个activity为NextActivity. 实

  • Android Activity切换(跳转)时出现黑屏的解决方法 分享

    在两个Activity跳转时,由于第二个Activity在启动时加载了较多数据,就会在启动之前出现一个短暂的黑屏时间,解决这个问题比较简单的处理方法是将第二个Activity的主题设置成透明的,这样在启动第二个Activity时的黑屏就变成了显示第一个Activity界面.这个分两步完成:第一步:xxx/res/values/styles.xml中加入自定义Activity的Theme,如下所示: [html]  <style name="Transparent" parent=

  • android PopupWindow 和 Activity弹出窗口实现方式

    本人小菜一个.目前只见过两种弹出框的实现方式,第一种是最常见的PopupWindow,第二种也就是Activity的方式是前几天才见识过.感觉很霸气哦.没想到,activity也可以做伪窗口. 先贴上最常见的方法,主要讲activity的方法. 一.弹出PopupWindow 复制代码 代码如下: /** * 弹出menu菜单 */ public void menu_press(){ if(!menu_display){ //获取LayoutInflater实例 inflater = (Layo

  • Android的Activity跳转动画各种效果整理

    大家使用Android的原生UI都知道,Android的Activity跳转就是很生硬的切换界面.其实Android的Activity跳转可以设置各种动画.下面给大家看看效果:  实现非常简单,用overridePendingtransition(int inId, int outId)即可实现.inId是下一界面进入效果的xml文件的id,outId是当前界面退出效果的xml文件id. 效果是用xml文件写的,首先要在res文件夹下建立anim文件夹,然后把动画效果xml文件放到里面去. 下面

  • Activity透明/半透明效果的设置transparent(两种实现方法)

    方法一:res/values文件夹下建立styles.xml: 复制代码 代码如下: <?xml version="1.0″ encoding="utf-8″?>  <style name="translucent"><item name="android:windowBackground">@color/translucent_background</item><item name=&quo

  • android获取当前运行Activity名字的方法

    本文实例讲述了android获取当前运行Activity名字的方法,可以避免即时聊天再出现通知的情况.分享给大家供大家参考.具体方法如下: 最近在做IM时需要知道当前Activity是哪一个Activity.自己整理一下两种方法 第一种:要方便一点(Service中无法使用) 复制代码 代码如下: private String getRunningActivityName() {          String contextString = context.toString();       

  • Android实现Activity界面切换添加动画特效的方法

    本文以实例形式展示了Android实现Activity界面切换添加动画特效的方法,对于Android程序设计人员来说有很好的参考借鉴价值.具体方法如下: 了解Android程序设计的人应该知道,在Android 2.0之后有了overridePendingTransition(),其中里面两个参数,一个是前一个activity的退出,另一个activity的进入. 现看看下面这段示例代码: @Override public void onCreate(Bundle savedInstanceSt

随机推荐