前言

说到Android视图大家第一反应肯定是Activity以及View,毕竟这是我们从入门开始接触最多的两个组件。但提到Activity和View之间联系以及设计背景可能会难道一大片人。其实关于视图系统还有一个重要概念Window不那么经常被提起,Android的设计者为了让开发者更容易上手,基于迪米特法则将Window屏蔽在内部。

本文将从设计背景为出发点阐述Activity、Window、View的实现流程,帮你换一种角度看Android视图系统,相信读完会让你耳目一新。

设计背景

View存在的意义

View直译过来就是视图的意思,是用来展示各种各样的视图。关于View的绘制流程相信大家都略知一二,共分为measure、layout、draw,前两步用来决定宽高和位置,真正绘制视图是在draw过程,通过onDraw方法传入的Canvas可以绘制各种各样的图形,而Canvas内部会通过JNI调用底层来实现真正的绘制操作。

既然Canvas就可以完成绘制那View存在的意义是什么呢?

想绘制出五彩斑斓的效果光有Canvas还远远不够,它还得配合Paint、Matrix等,这一系列操作让本就不简单的Canvas上手难度越来越高,复用率也越来越低,绘制各种复杂界面几乎成了不可完成的任务。面对这种痛点Android系统通过模板设计模式封装了一个用来管理绘制的组件View,屏蔽大量细节的同时提供三个模板方法measure、layout、draw,开发者可以通过View的三大模板方法自行定义视图的宽高、位置、形状,解决了大量模板代码以及复用率低的问题。

一个复杂的界面通常会包含很多元素比如文字、图片等,根据单一设计原则Android将其封装为TextView、ImageView。看起来万事大吉,但摆放这些View的时候又是一个大工程,各种坐标计算不 一会就晕头转向的,实际上摆放规则无非就那几种,所以Android利用View的layout特性封装了RelativeLayout、LinearLayout等layout用来控制各View之间的位置关系,进一步提升开发者效率。

如何管理错综复杂的View?

通过自定义View可以绘制出我们任意想要的效果,一切看似很美好。正当你盯着屏幕欣赏自己的作品时,“啪”糊上来一个界面,一通分析得知,原来其他app也通过View操控了屏幕,你也不甘示弱通过相同操作重新竞争到屏幕,如此反复进行 不可开交时屏幕黑了,得,还是换回塞班系统吧~~~

玩笑归玩笑,回归到问题本身。由于对View的管理不当造成了屏幕很混乱的情况。按常理来讲当用户在操作一个app时肯定不希望其他app蹦出来,所以在此背景下急需一套机制来管理错综复杂的View。于是Android在系统进程中创建了一个系统服务WindowManagerService(WMS)专门用来管理屏幕上的窗口,而View只能显示在对应的窗口上,如果不符合规定就不开辟窗口进而对应的View也无法显示

为什么WMS需要运行在系统进程?

由于每个app都对应一个进程,想要管理所有的应用进程,WMS需要找一个合适的地方能凌驾于所有应用进程之上,系统进程是最合适的选择

如何优雅的衔接各窗口之间关系?

自定义View可以定制各种视图效果,窗口可以让View有条不紊的显示,一切又美好了起来。但问题又来了,每个App都会有很多个界面(窗口),仅靠窗口/View来控制窗口和视图会面临一个很致命的问题:“不具备跳转和回退功能”。

按照我们的惯性思维,界面即可以跳转又可以回退,比如界面A跳转到界面B,按返回键理应退到界面A,这是一个标准的栈数据结构。

但关于界面的启动以及返回键的监听似乎与窗口不太搭嘎,所以Android基于单一设计原则封装了Activity,赋予窗口启动和回退功能,并由AMS统一调度Activity栈和生命周期。又通过迪米特法则将窗口的管理屏蔽在内部并暴露出onCreate、onStart、onResume...等模版方法,让开发者只专注于视图排版(View)和生命周期,无需关心窗口以及栈结构的存在。

所以,单纯说通过Activity创建一个界面似乎又不那么准确,一切窗口均源自于WMS,而窗口中内容由View进行填充,Activity只是在内部"间接"通过WMS管理窗口并协调好窗口与View的关系,最后再赋予栈、生命周期 等 功能而已。

tips

任务栈、返回栈并不是由Activity管理,Activity只是提供了任务栈、返回栈的必要条件,出入栈管理由AMS承担

关于Activity如何管理窗口View ? 请看下面。

实现流程

读源码的目的是为了理清设计流程,千万不要因果倒置陷入到代码细节当中,所以要懂得挑重点,讲究点到为止。本文为了提供更好的阅读体验,会将源码中大部分无用信息删掉,只保留精华。

Activity的由来

Activity从何而来?想追溯到源头,恐怕要到从开天辟地时造就第一个受精卵开始。

开天辟地造就的Zygote从何而来

Android系统会在开机时由Native层创建第一个进程init进程,随后init进程会解析一个叫init.rc的本地文件创建出Zygote进程。

字如其名,Zygote的职责就是孵化进程。当孵化出的第一个进程SystemServer进程后退居幕后,通过Socket静等创建进程的呼唤,一切应用进程均由Zygote进程孵化。

SystemServer进程的职责

SystemServer是Zygote自动创建的进程,并且会长时间驻留在内存中,该进程内部会注册各种Service,如:

  • ActivityManagerService(AMS):用来创建应用进程(通过socket ipc通知zygote进程)、管理四大组件

  • WindowManagerService(WMS):用来开辟和管理屏幕上的窗口,让视图有条不紊的显示

  • InputManagerService(IMS):用来处理和分发各种事件

  • 等等...

为什么要将这些系统服务放在单独进程?

像AMS、WMS、IMS都是用来处理一些系统级别的任务,比如Activity存在任务栈/返回栈的概念,如果在通过Activity进行应用间跳转时,需要协调好任务栈/返回栈的关系,而不同应用又属于不同进程,所以需要一个地方能凌驾于所有应用进程之上,而单独进程是最好的选择。关于WMS、IMS等其他Service同理,就不再赘述

应用进程的创建过程

前面说到AMS可以通知Zygote进程孵化应用进程,那究竟何时通知呢?其实大家应该已经猜到了,通过点击桌面上应用图标可以开启一个应用,所以AMS就是在此时通知Zygote创建应用进程。但桌面又是什么东西它从何而来?其实桌面也是一个Activity,它由AMS自动创建。

回归正题,点击应用图标到Activity的启动 这之间经历了什么流程?下面我简单列一下:

  • 当点击一个App图标时,如果对应的应用进程还没有创建则会通过Binder IPC通知到AMS创建应用进程

  • 应用进程启动后会执行我们所熟悉的main方法,而这个main方法则位于ActivityThread这个类中,main方法对应的就是Android主线程

  • ActivityThread的main方法首先会调用Looper.loop(),用来循环处理主线程Hanlder分发的消息。

  • 接下来的main方法会发送一个BIND_APPLICATION的消息,Looper收到后会通过Binder IPC通知AMS创建App进程对应的Application

  • Application创建后会再次通过Binder IPC通知AMS要创建Activity,AMS验证后会回到App进程,

  • 回到App进程后会间接调用ActivityThread#performLaunchActivity()来真正启动创建Activity,并且执行attach()和onCreate()。

tips

Application和Activity并不是通过AMS直接创建的,AMS只是负责管理和验证,真正创建具体对象还得到App进程。

Android视图系统是一个很庞大的概念,几乎贯穿了整个Java Framework,由于作者能力以及篇幅的原因,无法一文将Java Framework讲解清楚。所以就描述式的说了下系统进程、应用进程以及Activity的由来,尽可能你更清晰的认识Android视图系统。

PhoneWindow不等价于"Window(窗口)"

我之所以前面没有将窗口描述成Window是怕大家将二者混淆,因为应用进程的Window/PhoneWindow和真正的窗口根本就是两个概念,作者也曾在阅读源码时就这个问题困惑了很久。

Android SDK中的Window是一个抽象类,它有一个唯一实现类PhoneWindow,PhoneWindow内部会持有一个DecorView(根View),它的职责就是对DecorView做一些标准化的处理,比如标题、背景、导航栏、事件中转等,很显然与我们前面所说的窗口概念不符合。

那PhoneWindow何时被创建?

前面我提到可以通过ActivityThread#performLaunchActivity()创建Activity,来看下其代码:

#ActivityThreadprivate Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {...Activity activity = null;//注释1activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);...if (activity != null) {...//注释2.activity.attach(...);...//注释3.if (r.isPersistable()) {mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);} else {mInstrumentation.callActivityOnCreate(activity, r.state);}}...return activity;
}   

首先通过注释1处创建一个Activity对象,然后在注释2处执行其attach(..)方法,最后在通过callActivityOnCreate()执行Activity的onCreate()方法。

先来看attach做了什么事情:

#Activityfinal void attach(...){...mWindow = new PhoneWindow(this, window, activityConfigCallback);...mWindow.setWindowManager(...);mWindowManager = mWindow.getWindowManager();...
} 

Activity会在attach()方法中创建一个PhoneWindow对象并复制给成员变量mWindow,随后执行WindowManager的setter、getter。来重点看一下setter方法:

#Windowpublic void setWindowManager(...) {...if (wm == null) {//注释1wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);}//注释2mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);}

注释1处会通过系统服务获取一个WindowManager类型对象,用来管理Window。

注释2会通过WindowManager创建一个WindowManagerImpl对象,实际上WindowManager是一个接口,它继承自ViewManager接口,而WindowManagerImpl是它的一个实现类。

绕来绕去原来是通过WindowManager创建了另一个WindowManager,看起来多此一举,那Android为什么要这样设计呢?

首先WindowManager具备两个职责,管理Window和创建WindowManager。系统服务获取的WindowManager具备创建Window功能,但此时并未与任何Window关联。而通过createLocalWindowManager创建的WindowManager会与对应的Window一对一绑定。所以前者用于创建WindowManager,后者用于与Window一对一绑定,二者职责明确,但让作者费解的是为什么不基于单一设计原则把创建过程抽取至另一个类?如果有知道的同学可以评论区留言,事先谢过~

关于WindowManagerImpl如何管理Window先暂且不提,下面文章会说到。

PhoneWindow已经创建完毕,但还没有跟Activity/View做任何关联。扒一扒PhoneWindow的源码你会发现,它内部只是设置了标题、背景以及事件的中转等工作,与窗口完全不搭嘎,所以切勿将二者混淆

DecorView的创建时机

由上可知 Activity的attach()运行完毕后会执行onCreate(),通常我们需要在onCreate()中执行stContentView()才能显示的XML Layout。关于stContentView() 顾名思义就是设置我们的Content View嘛,内部代码如下:

#Activitypublic void setContentView(@LayoutRes int layoutResID) {getWindow().setContentView(layoutResID);...
}public Window getWindow() {return mWindow;
}

首先通过getWindow()获取到attach()阶段创建的PhoneWindow,随后将layoutResID(XML Layout)传递进去,继续跟:

#PhoneWindowViewGroup mContentParent;public void setContentView(int layoutResID) {//注释1if (mContentParent == null) {installDecor();} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {mContentParent.removeAllViews();}if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {...} else {//注释2mLayoutInflater.inflate(layoutResID, mContentParent);}}

注释1处会判断mContentParent是否为空,如果为空会通过installDecor()对其实例化,否则移除所有子View。

注释2处会将layoutResID对应的XML加载到mContentParent。到此为止唯一的疑问是mContentParent如何被创建的,跟一下installDecor():

#PhoneWindowprivate void installDecor() {if (mDecor == null) {mDecor = generateDecor(-1);...} else {mDecor.setWindow(this);}if (mContentParent == null) {mContentParent = generateLayout(mDecor);...}}

首先创建DecorView类型对象并赋值给引用mDecor。那什么是DecorView?

DecorView继承自FrameLayout,内部有一个垂直布局的LinearLayout用来摆放状态栏背景、TitleBar、ContentView、导航栏背景,其中ContentView就是用来存放由Activity#setContentView传入的Layout。之所以设计出DecorView,是因为部分场景状态栏背景、标题栏、导航栏背景等需要系统统一管理。最后基于迪米特法则将其管理操作屏蔽在内部,只暴露出ContentView由开发者填充。

注意,DecorView只是给状态栏、导航栏填充了背景,电量、信号..由系统统一绘制

再回到mDecor的创建过程,跟一下generateDecor(-1)代码:

#PhoneWindowprotected DecorView generateDecor(int featureId) {...return new DecorView(context, featureId, this, getAttributes());
}

直接new出来了一个DecorView。再回到我们最初的疑问,mContentParent从何而来?installDecor()创建出DecorView会通过generateLayout(mDecor)创建mContentParent。generateLayout(mDecor)代码很长就不贴了,内部会通过mDecor获取到mContentParent并为其设置主题、背景等。

到此阶段DecorView创建完毕并与XML Layout建立了关联,但此时根View(DecorView)还未与窗口建立关联,所以是看不到的。

为什么要在onCreate执行setContentView?

通过setContentView可以创建DecorView,而一个Activity通常只有一个DecorView(撇去Dialog等),如若将setContentView放在start、resume可能会创建多个DecorView,进而会造成浪费。所以onCreate是创建DecorView的最佳时机

ViewRootImpl如何协调View和Window的关系?

Activity启动后会在不同时机通过ActivityThread调用对应的生命周期方法,onResume是一个特殊的时机它通过ActivityThread#handleResumeActivity被调用,代码如下:

#PhoneWindowpublic void handleResumeActivity(...) {//注释1final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);...final Activity a = r.activity;...//注释2r.window = r.activity.getWindow();View decor = r.window.getDecorView();decor.setVisibility(View.INVISIBLE);ViewManager wm = a.getWindowManager();WindowManager.LayoutParams l = r.window.getAttributes();...//注释3wm.addView(decor, l);...}
  • 注释1处 会间接调用Activity的onResume方法

  • 注释2处 通过Activity获取PhoneWindow、DecorView、WindowManager,它们的创建时机前面小结有写,忘记的可以回翻阅读。

  • 注释3处 调用了WindowManager的addView方法,顾名思义就是将DecorView添加至Window当中,这一步非常关键

关于WindowManager的概念已经提到过,它是一个接口有一个实现类WindowManagerImp,跟一下其addView()方法。

#WindowManagerImpprivate final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();public void addView(...) {...mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow, mContext.getUserId());...}

内部调用了mGlobal的addView()方法,其实不光addView几乎所有WindowManager方法都是通过委托mGlobal去实现,这种写法看似很奇怪,但实际上这种设计不仅不奇怪而且还很精妙,具体精妙在何处?我列出以下三点:

  • WindowManager提供的功能全局通用不会与某个View/Window单独绑定,为了节省内存理应设计出一个单例。

  • WindowManagerImp具备多个职责如Token管理、WindowManager功能等,所以通过单一设计原则将WindowManager功能拆分到另一个类中即WindowManagerGlobal,并将其定义为单例。

  • 为了不违背迪米特法则又通过组合模式将WindowManagerGlobal屏蔽在内部。

回归正题,来看mGlobal的addView()方法:

#WindowManagerGlobal
/*** 用来存储所有的DecorView*/
private final ArrayList<View> mViews = new ArrayList<View>();
/*** 用来存储所有的ViewRootImpl*/
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
/*** 用来存储所有的LayoutParams*/
private final ArrayList<WindowManager.LayoutParams> mParams =new ArrayList<WindowManager.LayoutParams>();public void addView(...) {...ViewRootImpl root;synchronized (mLock) {root = new ViewRootImpl(view.getContext(), display);mViews.add(view);mRoots.add(root);mParams.add(wparams);...root.setView(view, wparams, panelParentView, userId);...}}

首先创建一个ViewRootImpl类型对象root,然后将view、root、wparams加入到对应的集合,由WindowManagerGlobal的单例对象统一管理,最后执行root的setView()。根据我多年阅读源码的经验 答案应该就在root.setView()里,继续跟。

ViewRootImpl
public void setView(...) {synchronized (this) {if (mView == null) {...mView = view;...//注释1requestLayout();//注释2res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mDisplayCutout, inputChannel,mTempInsets, mTempControls);...//注释3view.assignParent(this);}}}void assignParent(ViewParent parent) {if (mParent == null) {mParent = parent;} else if (parent == null) {mParent = null;}...
}

ViewRootImpl#setView()方法很长,我做了下精简列出几个关键步骤:

  • 注释1,requestLayout()通过一系列调用链最终会开启mView(DecorView)的绘制(measure、layout、draw)。这一流程很复杂,由于篇幅原因本文就不提了,感兴趣的可查阅Choreographer相关知识

  • 注释2,mWindowSession是一个IWindowSession类型的AIDL文件,它会通过Binder IPC通知WMS在屏幕上开辟一个窗口,关于WMS的实现流程也非常庞大,我们点到为止。这一步执行完我们的View就可以显示到屏幕上了

  • 注释3,最后一步执行了View#assignParent,内部将mParent设置为ViewRootImpl。所以,虽然ViewRootImpl不是一个View,但它是所有View的顶层Parent

小结开头我有提到,好多人将API中的Window/PhoneWindow等价于窗口,但实际上操作开辟窗口的是ViewRootImpl,并且负责管理View的绘制,是整个视图系统最关键的一环。

疑惑

经常听到有人说onStart阶段处于可见模式,对此我感到疑惑。通过源码的分析可知onResume执行完毕后才会创建窗口并开启DecorView的绘制,所以在onStart连窗口都没有何谈可见?

注意点

初学Android时经常在onCreate时机获取View宽高而犯错,原因是View是在onResume后才开始绘制,所以在此之前无法获取到View宽高状态,此时可以通过View.post{}或者addOnGlobalLayoutListener来获取宽高。

Java Framework层面视图系统的实现非常复杂,为了方便大家理解,我列出提到的几个关键类和对应的职责。

  • Window是一个抽象类,通过控制DecorView提供了一些标准的UI方案,比如背景、标题、虚拟按键等

  • PhoneWindow是Window的唯一实现类,完善了Window的功能,并提供了事件的中转

  • WindowManager是一个接口,继承自ViewManager接口,提供了View的基本操作方法

  • WindowManagerImp实现了WindowManager接口,内部通过组合方式持有WindowManagerGlobal,用来操作View

  • WindowManagerGlobal是一个全局单例,内部可以通过ViewRootImpl将View添加至窗口中

  • ViewRootImpl是所有View的Parent,用来管理View的绘制以及窗口的开辟

  • IWindowSession是IWindowSession类型的AIDL接口,可以通过Binder IPC通知WMS开辟窗口

至此关于Java Framework层面视图系统的设计与实现梳理完毕。

综上所述

  • 一切视图均由Canvas而来

  • View的出现是为了提供视图模板,用来提升开发效率

  • 窗口可以让View有条不紊的显示

  • Activity给每个窗口增加生命周期,让窗口切换更加优雅

  • PhoneWindow只是提供些标准的UI方案,与窗口不等价

  • 可通过WindowManager将View添加到窗口

  • ViewRootImpl才是开辟窗口的那个角色,并管理View的绘制,是视图系统最关键的一环

  • 错综复杂的视图系统基本都隐藏Activity内部,开发者只需基于模板方法即可开发

    原文链接 https://juejin.cn/user/2629687546479742

关注我获取更多知识或者投稿

通俗易懂,Android视图系统的设计与实现相关推荐

  1. 学了又忘?通俗易懂 Android视图系统的设计与实现!

    前言 说到Android视图大家第一反应肯定是Activity以及View,毕竟这是我们从入门开始接触最多的两个组件.但提到Activity和View之间联系以及设计背景可能会难道一大片人.其实关于视 ...

  2. Android 视图系统的设计与实现 | 通俗易懂

    Bezier | 作者 承香墨影 | 编辑 https://juejin.cn/post/6865625913309511687 | 原文 郭霖(ID:guolin_blog) | 转自 前言 Hi, ...

  3. Android视图系统的设计与实现

    前言 说到Android视图大家第一反应肯定是Activity以及View,毕竟这是我们从入门开始接触最多的两个组件.但提到Activity和View之间联系以及设计背景可能会难道一大片人.其实关于视 ...

  4. 一篇通俗易懂的Android视图系统设计与实现

    好文推荐 作者:Bezier 前言 说到Android视图大家第一反应肯定是Activity以及View,毕竟这是我们从入门开始接触最多的两个组件.但提到Activity和View之间联系以及设计背景 ...

  5. 基于Vue和SpringBoot的网上蛋糕销售系统的设计和实现

    作者主页:Designer 小郑 作者简介:Java全栈软件工程师一枚,来自浙江宁波,负责开发管理公司OA项目,专注软件前后端开发(Vue.SpringBoot和微信小程序).系统定制.远程技术指导. ...

  6. 基于Android的天气预报系统的设计与实现

    目 录 1 绪论 1 1.1课题研究背景及意义 1 1.2国内外研究动态 1 1.2.1国外发展现状 2 1.2.2国内发展现状 3 1.3论文研究的主要内容 3 1.4论文组织结构 4 2 系统关键 ...

  7. android 个人理财系统,基于Android的个人理财系统的设计与实现

    摘要: 在当今,全球经济的蓬勃发展带来了金融理财领域的巨大变革和创新,新的金融理财工具和理财观点层出不穷.对个人而言,从认识钱到管理钱(个人理财)是每个人的必修课.在Android系统应用如此火热的情 ...

  8. 基于android的视频采集系统的设计与实现,基于Android的视频通话系统的设计与实现.docx...

    基于Android的视频通话系统的设计与实现 基于Android的视频通话系统的设计与实现摘 要近年来,智能手机操作系统发展迅速,尤其是Android系统的迅猛发展已经将全球智能手机市场引领到了非常火 ...

  9. Android源码分析(三)-----系统框架设计思想

    一 : 术在内而道在外 Android系统的精髓在源码之外,而不在源码之内,代码只是一种实现人类思想的工具,仅此而已...... 近来发现很多关于Android文章都是以源码的方向入手分析Androi ...

最新文章

  1. C#开发WPF/Silverlight动画及游戏系列教程(Game Tutorial):(二十五)完美捕捉精灵之神器 -- HitTest...
  2. JavaScript教程之DOM和BOM
  3. 在云服务器上注意GeoServer和ShadowDataMap的跨域设置
  4. C语言简单的日期校验函数
  5. 关于Zuul的几个问题
  6. Golang 【大字符串相加,求和】
  7. libcurl linux 静态链接库_GCC 程序编译的静态链接和动态链接
  8. windows下重设mysql的root密码
  9. 华硕固件默认ip_斐讯刷华硕固件后开启QOS限速的方法
  10. DSP2812入门5——使用C语言操作DSP寄存器
  11. 数学分析教程(科大)——5.2笔记+习题
  12. 64位x86微服务器芯片,卖贝商城告诉你微服务器替代x86服务器的利与弊
  13. numpy tolist()
  14. vue 两种文档下载方法的实现(后台传递文件流,后台返回文件下载地址)
  15. mysql backup 使用_MySQLBackup 使用说明
  16. Linux强制退出当前执行命令
  17. 程序员薪水最高的25家公司
  18. Pluecker coordinates普吕克坐标系介绍
  19. Codeforces 786 A. Berzerk
  20. 2021年苍南桥墩高中高考成绩查询,2021年温州各高中高考成绩排名及放榜最新消息...

热门文章

  1. ArcGIS Image Server简介以及OL2中的加载
  2. NOIP2014 飞扬的小鸟 题解
  3. VMware vCenter Server 7.0 完整安装教程
  4. 健康小贴士(肚子疼)
  5. 南大通用GBase8s 常用SQL语句(168)
  6. 压缩感知重构算法之正交匹配追踪算法(OMP)
  7. APICloud资料
  8. GICv3软件overview手册之GICv3基本功能(5)
  9. 【云栖大会】飞天进化:从操作系统到人工智能
  10. 1篇SCI一区/3篇二区认定A类博士!免费120㎡住房+78万引进费!4500/月博士津贴,直接副教授待遇!...