Android如何在应用层进行截屏及截屏源码分析(上)

2024-08-31 00:18

本文主要是介绍Android如何在应用层进行截屏及截屏源码分析(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近在看framework层代码时发现其中有一个是测试截屏操作的专门的包,于是潜意识的驱使下就研究了这方面的知识,今天作个总结吧!以及我们在写上层应用时如何做截屏操作的,那么我们先来看看截屏的源码分析,其实截屏操作就java这部分是放在了系统SystemUI那里,用过android系统手机的同学应该都知道,一般的android手机按下音量减少键和电源按键就会触发截屏事件(国内定制机做个修改的这里就不做考虑了)

我们知道这里的截屏事件是通过我们的按键操作触发的,所以这里就需要我们从android系统的按键触发模块开始看起,由于我们在不同的App页面,操作音量减少键和电源键都会触发系统的截屏处理,所以这里的按键触发逻辑应该是Android系统的全局按键处理逻辑。

在android系统中,由于我们的每一个Android界面都是一个Activity,而界面的显示都是通过Window对象实现的,每个Window对象实际上都是PhoneWindow的实例,而每个PhoneWindow对象都一个PhoneWindowManager对象,当我们在Activity界面执行按键操作的时候,在将按键的处理操作分发到App之前,首先会回调PhoneWindowManager中的dispatchUnhandledKey方法,按键分发处理,见名知意该方法主要用于执行当前App处理按键之前的操作,PhoneWindowManager所在包如下图所示:

这里写图片描述

那么在我们看该方法是怎么实现时先来看看这个方法在哪被调用吧,

这里写图片描述

这里写图片描述

ctrl+Shift+G可以发现PhoneWindowManager的dispatchUnhandledKey方法在InputManagerService的dispatchUnhandledKey执行,而InputManagerService.dispatchUnhandledKey是一个Native callback.,学过NDK的人都知道这个方法看来是通过JNI回调了,即是硬件驱动屏检测到按键输入再包装到库层通过C++实现,再C++那边调用了该方法传回一些参数然后传给我们上层操作。

在InputManagerService.dispatchUnhandledKey方法中通过一个mWindowManagerCallbacks实现,那么我们再看看mWindowManagerCallbacks吧,该mWindowManagerCallbacks是InputManagerService的内部接口
这里写图片描述

这里写图片描述

该接口的实例是被作为观察者模式传进来的,在SystemServer中传入

这里写图片描述

而InputManagerService又作为Android的一个服务被添加到SystemServer中,如果对这方面不是很了解的同学,请移步至Android开发如何定制framework层服务 作个具体的了解。

看上图可知mWindowManagerCallbacks最终是通过WindowManagerService.getInputMonitor()得到的,我们去WindowManagerService这个服务里面看看这个getInputMonitor方法:
这里写图片描述
该方法返回一个InputMonitor,我们再进去InputMonitor看看什么情况:
这里写图片描述
该InputMonitor实现了InputManagerService.WindowManagerCallbacks这个接口,dispatchUnhandledKey方法如下:
这里写图片描述
看图可知dispatchUnhandledKey该方法最后还是通过WindowManagerService这个Android服务来实现的,对Android而言所有的UI都是通过WindowManagerService这个服务去操作;

接下来我们再继续看一下具体该方法的实现。

 /** {@inheritDoc} */@Overridepublic KeyEvent dispatchUnhandledKey(WindowState win, KeyEvent event, int policyFlags) {// Note: This method is only called if the initial down was unhandled.if (DEBUG_INPUT) {Slog.d(TAG, "Unhandled key: win=" + win + ", action=" + event.getAction()+ ", flags=" + event.getFlags()+ ", keyCode=" + event.getKeyCode()+ ", scanCode=" + event.getScanCode()+ ", metaState=" + event.getMetaState()+ ", repeatCount=" + event.getRepeatCount()+ ", policyFlags=" + policyFlags);}KeyEvent fallbackEvent = null;if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {final KeyCharacterMap kcm = event.getKeyCharacterMap();final int keyCode = event.getKeyCode();final int metaState = event.getMetaState();final boolean initialDown = event.getAction() == KeyEvent.ACTION_DOWN&& event.getRepeatCount() == 0;// Check for fallback actions specified by the key character map.final FallbackAction fallbackAction;if (initialDown) {fallbackAction = kcm.getFallbackAction(keyCode, metaState);} else {fallbackAction = mFallbackActions.get(keyCode);}if (fallbackAction != null) {if (DEBUG_INPUT) {Slog.d(TAG, "Fallback: keyCode=" + fallbackAction.keyCode+ " metaState=" + Integer.toHexString(fallbackAction.metaState));}final int flags = event.getFlags() | KeyEvent.FLAG_FALLBACK;fallbackEvent = KeyEvent.obtain(event.getDownTime(), event.getEventTime(),event.getAction(), fallbackAction.keyCode,

该方法主要是包装了一下KeyEvent event,关键代码如下:
这里写图片描述
记住这个红色框框部分,该部分再下面要说到,那么这里将KeyEvent 放到了interceptFallback这个方法中处理了,我们再进去这个interceptFallback里面看看吧,如下:
这里写图片描述

然后我们看到在interceptFallback方法中我们调用了interceptKeyBeforeQueueing方法,通过阅读我们我们知道该方法主要实现了对截屏按键的处理流程,这样我们继续看一下interceptKeyBeforeWueueing方法的处理,该方法比较长:

  /** {@inheritDoc} */@Overridepublic int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags, boolean isScreenOn) {if (!mSystemBooted) {// If we have not yet booted, don't let key events do anything.return 0;}final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;final boolean canceled = event.isCanceled();final int keyCode = event.getKeyCode();final boolean isInjected = (policyFlags & WindowManagerPolicy.FLAG_INJECTED) != 0;// If screen is off then we treat the case where the keyguard is open but hidden// the same as if it were open and in front.// This will prevent any keys other than the power button from waking the screen// when the keyguard is hidden by another activity.final boolean keyguardActive = (mKeyguardMediator == null ? false :(isScreenOn ?mKeyguardMediator.isShowingAndNotHidden() :mKeyguardMediator.isShowing()));if (keyCode == KeyEvent.KEYCODE_POWER) {policyFlags |= WindowManagerPolicy.FLAG_WAKE;}final boolean isWakeKey = (policyFlags & (WindowManagerPolicy.FLAG_WAKE| WindowManagerPolicy.FLAG_WAKE_DROPPED)) != 0;if (DEBUG_INPUT) {Log.d(TAG, "interceptKeyTq keycode=" + keyCode+ " screenIsOn=" + isScreenOn + " keyguardActive=" + keyguardActive+ " policyFlags=" + Integer.toHexString(policyFlags)+ " isWakeKey=" + isWakeKey);}if (down && (policyFlags & WindowManagerPolicy.FLAG_VIRTUAL) != 0&& event.getRepeatCount() == 0) {performHapticFeedbackLw(null, HapticFeedbackConstants.VIRTUAL_KEY, false);}// Basic policy basedn screen state and keyguard.// FIXME: This policy isn't quite correct.  We shouldn't care whether the screen//        is on or off, really.  We should care about whether the device is in an//        interactive state or is in suspend pretending to be "off".//        The primary screen might be turned off due to proximity sensor or//        because we are presenting media on an auxiliary screen or remotely controlling//        the device some other way (which is why we have an exemption here for injected//        events).int result;if ((isScreenOn && !mHeadless) || (isInjected && !isWakeKey)) {// When the screen is on or if the key is injected pass the key to the application.result = ACTION_PASS_TO_USER;} else {// When the screen is off and the key is not injected, determine whether// to wake the device but don't pass the key to the application.result = 0;if (down && isWakeKey && isWakeKeyWhenScreenOff(keyCode)) {if (keyguardActive) {// If the keyguard is showing, let it wake the device when ready.mKeyguardMediator.onWakeKeyWhenKeyguardShowingTq(keyCode);} else {// Otherwise, wake the device ourselves.result |= ACTION_WAKE_UP;}}}// Handle special keys.    switch (keyCode) {case KeyEvent.KEYCODE_VOLUME_DOWN:case KeyEvent.KEYCODE_VOLUME_UP:case KeyEvent.KEYCODE_VOLUME_MUTE: {if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {if (down) {if (isScreenOn && !mVolumeDownKeyTriggered&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {mVolumeDownKeyTriggered = true;mVolumeDownKeyTime = event.getDownTime();mVolumeDownKeyConsumedByScreenshotChord = false;cancelPendingPowerKeyAction();interceptScreenshotChord();}} else {mVolumeDownKeyTriggered = false;cancelPendingScreenshotChordAction();}} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {if (down) {if (isScreenOn && !mVolumeUpKeyTriggered&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {mVolumeUpKeyTriggered = true;cancelPendingPowerKeyAction();cancelPendingScreenshotChordAction();}} else {mVolumeUpKeyTriggered = false;cancelPendingScreenshotChordAction();}}if (down) {  ITelephony telephonyService = getTelephonyService();if (telephonyService != null) {try {if (telephonyService.isRinging()) {// If an incoming call is ringing, either VOLUME key means// "silence ringer".  We handle these keys here, rather than// in the InCallScreen, to make sure we'll respond to them// even if the InCallScreen hasn't come to the foreground yet.// Look for the DOWN event here, to agree with the "fallback"// behavior in the InCallScreen.Log.i(TAG, "interceptKeyBeforeQueueing:"+ " VOLUME key-down while ringing: Silence ringer!");// Silence the ringer.  (It's safe to call this// even if the ringer has already been silenced.)telephonyService.silenceRinger();// And *don't* pass this key thru to the current activity// (which is probably the InCallScreen.)result &= ~ACTION_PASS_TO_USER;break;}if (telephonyService.isOffhook()&& (result & ACTION_PASS_TO_USER) == 0) {// If we are in call but we decided not to pass the key to// the application, handle the volume change here.handleVolumeKey(AudioManager.STREAM_VOICE_CALL, keyCode);break;}} catch (RemoteException ex) {Log.w(TAG, "ITelephony threw RemoteException", ex);}}if (isMusicActive() && (result & ACTION_PASS_TO_USER) == 0) {// If music is playing but we decided not to pass the key to the// application, handle the volume change here.handleVolumeKey(AudioManager.STREAM_MUSIC, keyCode);break;}}break;}case KeyEvent.KEYCODE_ENDCALL: { result &= ~ACTION_PASS_TO_USER;if (down) {ITelephony telephonyService = getTelephonyService();boolean hungUp = false;if (telephonyService != null) {try {hungUp = telephonyService.endCall();} catch (RemoteException ex) {Log.w(TAG, "ITelephony threw RemoteException", ex);}}interceptPowerKeyDown(!isScreenOn || hungUp);} else {if (interceptPowerKeyUp(canceled)) {if ((mEndcallBehavior& Settings.System.END_BUTTON_BEHAVIOR_HOME) != 0) {if (goHome()) {break;}}if ((mEndcallBehavior& Settings.System.END_BUTTON_BEHAVIOR_SLEEP) != 0) {result = (result & ~ACTION_WAKE_UP) | ACTION_GO_TO_SLEEP;}}}break;}case KeyEvent.KEYCODE_POWER: { result &= ~ACTION_PASS_TO_USER;if (down) {if (isScreenOn && !mPowerKeyTriggered&& (event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {mPowerKeyTriggered = true;mPowerKeyTime = event.getDownTime();interceptScreenshotChord();}ITelephony telephonyService = getTelephonyService();boolean hungUp = false;if (telephonyService != null) {try {if (telephonyService.isRinging()) {// Pressing Power while there's a ringing incoming// call should silence the ringer.telephonyService.silenceRinger();} else if ((mIncallPowerBehavior& Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP) != 0&& telephonyService.isOffhook()) {// Otherwise, if "Power button ends call" is enabled,// the Power button will hang up any current active call.hungUp = telephonyService.endCall();}} catch (RemoteException ex) {Log.w(TAG, "ITelephony threw RemoteException", ex);}}interceptPowerKeyDown(!isScreenOn || hungUp|| mVolumeDownKeyTriggered || mVolumeUpKeyTriggered);} else {mPowerKeyTriggered = false;cancelPendingScreenshotChordAction();if (interceptPowerKeyUp(canceled || mPendingPowerKeyUpCanceled)) {result = (result & ~ACTION_WAKE_UP) | ACTION_GO_TO_SLEEP;}mPendingPowerKeyUpCanceled = false;}break;}case KeyEvent.KEYCODE_MEDIA_PLAY:case KeyEvent.KEYCODE_MEDIA_PAUSE:case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:if (down) {ITelephony telephonyService = getTelephonyService();if (telephonyService != null) {try {if (!telephonyService.isIdle()) {// Suppress PLAY/PAUSE toggle when phone is ringing or in-call// to avoid music playback.break;}} catch (RemoteException ex) {Log.w(TAG, "ITelephony threw RemoteException", ex);}}}case KeyEvent.KEYCODE_HEADSETHOOK:case KeyEvent.KEYCODE_MUTE:case KeyEvent.KEYCODE_MEDIA_STOP:case KeyEvent.KEYCODE_MEDIA_NEXT:case KeyEvent.KEYCODE_MEDIA_PREVIOUS:case KeyEvent.KEYCODE_MEDIA_REWIND:case KeyEvent.KEYCODE_MEDIA_RECORD:case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {if ((result & ACTION_PASS_TO_USER) == 0) { // Only do this if we would otherwise not pass it to the user. In that// case, the PhoneWindow class will do the same thing, except it will// only do it if the showing app doesn't process the key on its own.// Note that we need to make a copy of the key event here because the// original key event will be recycled when we return.mBroadcastWakeLock.acquire();Message msg = mHandler.obtainMessage(MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK,new KeyEvent(event));msg.setAsynchronous(true);msg.sendToTarget();}break;}case KeyEvent.KEYCODE_CALL: {if (down) {ITelephony telephonyService = getTelephonyService();if (telephonyService != null) {try {if (telephonyService.isRinging()) {Log.i(TAG, "interceptKeyBeforeQueueing:"+ " CALL key-down while ringing: Answer the call!");telephonyService.answerRingingCall();// And *don't* pass this key thru to the current activity// (which is presumably the InCallScreen.)result &= ~ACTION_PASS_TO_USER;}} catch (RemoteException ex) {Log.w(TAG, "ITelephony threw RemoteException", ex);}}}break;}}return result;}

可以发现这里首先判断当前系统是否已经boot完毕,若尚未启动完毕,则所有的按键操作都将失效,若启动完成,则执行后续的操作,这里我们只是关注音量减少按键和电源按键组合的处理事件。另外这里多说一句像安卓系统的HOME按键事件,MENU按键事件,进程列表按键事件等等都是在这里实现的
我们关注一下关键代码部分就好了,看看按键捕获部分,如下:

这里写图片描述
当我用按下音量减少按键的时候回进入到:case KeyEvent.KEYCODE_VOLUME_MUTE分支并执行相应的逻辑,然后同时判断用户是否按下了电源键,若同时按下了电源键,则执行:

这里写图片描述

见名知意可以发现这里的interceptScreenshotChrod方法就是系统准备开始执行截屏操作的开始,我们继续看一下interceptcreenshotChord方法的实现。

这里写图片描述

在方法体中我们最终会执行发送一个延迟的异步消息,请求执行截屏的操作而这里的延时时间,若当前输入框是打开状态,则延时时间为输入框关闭时间加上系统配置的按键超时时间,若当前输入框没有打开则直接是系统配置的按键超时处理时间,再看看mScreenshotChordLongPress这个Runnable的具体实现。

这里写图片描述

方法体中并未执行其他操作,直接就是调用了takeScreenshot方法,这样我们继续看一下takeScreenshot方法的实现。

 // Assume this is called from the Handler thread.private void takeScreenshot() {synchronized (mScreenshotLock) {if (mScreenshotConnection != null) {return;}ComponentName cn = new ComponentName("com.android.systemui","com.android.systemui.screenshot.TakeScreenshotService");Intent intent = new Intent();intent.setComponent(cn);ServiceConnection conn = new ServiceConnection() {@Overridepublic void onServiceConnected(ComponentName name, IBinder service) {synchronized (mScreenshotLock) {if (mScreenshotConnection != this) {return;}Messenger messenger = new Messenger(service);Message msg = Message.obtain(null, 1);final ServiceConnection myConn = this;Handler h = new Handler(mHandler.getLooper()) {@Overridepublic void handleMessage(Message msg) {synchronized (mScreenshotLock) {if (mScreenshotConnection == myConn) {mContext.unbindService(mScreenshotConnection);mScreenshotConnection = null;mHandler.removeCallbacks(mScreenshotTimeout);}}}};msg.replyTo = new Messenger(h);  msg.arg1 = msg.arg2 = 0;if (mStatusBar != null && mStatusBar.isVisibleLw())msg.arg1 = 1;if (mNavigationBar != null && mNavigationBar.isVisibleLw())msg.arg2 = 1;try {messenger.send(msg);} catch (RemoteException e) {}}}@Overridepublic void onServiceDisconnected(ComponentName name) {}};if (mContext.bindService(intent, conn, Context.BIND_AUTO_CREATE, UserHandle.USER_CURRENT)) {mScreenshotConnection = conn;mHandler.postDelayed(mScreenshotTimeout, 10000);}}}

那么看代码可知这里是启动了一个TakeScreenshotService,该service即是最上面的图中systemUI下的,我们再看看TakeScreenshotService这个类里面到底做了什么事吧!

这里写图片描述

该service在被成功绑定时候回有一个handler回调过来然后拿到一个GlobalScreenshot去执行takeScreenshot方法,好吧,继续看一下takeScreentshot方法的执行逻辑。

这里写图片描述

该方法后面有两个参数:statusBarVisible,navBarVisible是否可见,而这两个参数在我们PhoneWindowManager.takeScreenshot方法传递的,在我们启动TakeScreenshotService时传入:

这里写图片描述

可见若果状态条可见,则传递的statusBarVisible为true,若导航条可见,则传递的navBarVisible为true。然后我们在截屏的时候判断nStatusBar是否可见,mNavigationBar是否可见,若可见的时候则截屏同样将其截屏出来。

再来看看GlobalScreenshot.takeScreenshot方法中截屏最关键的代码:
这里写图片描述
看注释可知,这里就是执行截屏事件的具体操作了,然后我看一下SurfaceControl.screenshot方法的具体实现,另外这里需要注意的是,截屏之后返回的是一个Bitmap对象,其实熟悉android绘制机制的朋友都应该要知道android中所有显示能够显示的东西,在内存中表现都是Bitmap对象。
这里写图片描述
如图可知,那么这个Surface.screenshot方法被@了一个hide,看来是被Google隐藏掉了,该方法最后是调用了本地方法nativeScreenshot函数,这个是在C++那边操作的,具体的实现在JNI层,由于个人对C++不是很熟练那么这边不做过多的介绍。framework中间层和HAL库函数打交道基本上都是这个模式。

另外在GlobalScreenshot.takeScreenshot这个方法中其它是做了一些动画,通知等操作,比如我们截屏时保存的图片会有一个动画以及截屏成功之后会在通知栏收到一条通知等。

这篇关于Android如何在应用层进行截屏及截屏源码分析(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1122342

相关文章

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟 开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚 第一站:海量资源,应有尽有 走进“智听

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

业务中14个需要进行A/B测试的时刻[信息图]

在本指南中,我们将全面了解有关 A/B测试 的所有内容。 我们将介绍不同类型的A/B测试,如何有效地规划和启动测试,如何评估测试是否成功,您应该关注哪些指标,多年来我们发现的常见错误等等。 什么是A/B测试? A/B测试(有时称为“分割测试”)是一种实验类型,其中您创建两种或多种内容变体——如登录页面、电子邮件或广告——并将它们显示给不同的受众群体,以查看哪一种效果最好。 本质上,A/B测

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者