【Android】Android Framework系列---输入法服务_android 输入法-程序员宅基地

技术标签: ime  调试  pinyin  inputmethod  输入法  android  ims  AndroidFramework  

Android Framework系列之输入法服务

  • 本文基于Android R(11),从Framework角度介绍Android输入法框架流程及常用调试方法。
    输入法脑图

写在前面

车载项目需要定制输入法,也有一些POC演示的项目使用原生比如LatinIME(源码路径为/packages/inputmethods/LatinIME),关于输入法可能会遇到以下一些问题

  • 输入法进程启动崩溃
  • 输入法画面被其他应用遮挡
  • 输入法输入内容显示到错误的编辑框内
  • 多屏情况下输入法显示异常
  • 输入法未弹出或输入法未隐藏
  • 定制多屏多客户端输入法

上面举了一些常见例子,实际开发过程中也会有定制输入法服务这类需求。所以对于Android输入法,作为Android Framework工程师对其要有一个整体框架性的了解。

专用术语

  • IMMS: InputMethodManagerService
  • IMS: InputMethodService
  • IMM: InputMethodManager
  • IME: InputMethodEditor
  • MCIMMS:MultiClientInputMethodManagerService

输入法知识点

输入法框架

Android输入法框架包括:IMMS输入法管理服务、IMS输入法服务、IMM输入法管理(客户端)。

  1. IMMS:顾名思义,用于管理输入法的Service,包括打开、关闭、显示、隐藏、切换、绑定输入法等等。这个Service运行在SystemServer中。另外,Android中引入了MCIMMS用于支持多个输入法Client,MCIMMS目前仅作为一个Test功能,感兴趣的可自行研究。
  2. IMS: 输入法服务,比如Android原生自带的LatinIME通过继承InputMethodService的方式实现了一个IMS。IMS以 Application Service的形式运行在应用进程中,通过IMMS管理其状态(比如打开输入法)。
    LatinIME
  3. IMM: 输入法管理(客户端),Android中经常将Client端被命名为 XXManager,比如AudioManager,WindowManager,输入法的客户端也是这样。IMM主要指InputMethodManager这个单例类,应用进程通过这个单例对象与IMMS/IMS进行交互。

在这里插入图片描述

输入法的启动
IMMS初始化

在这里插入图片描述

  • Kernel拉起Init进程,Init启动Zygote,Zyogte启动SystemServer。SystemServer在startOtherServices阶段启动 IMMS,代码如下(本文下述代码中省略了部分源码
// SystemServer.java
public static void main(String[] args) {
    
      new SystemServer().run();
}

// SystemServer.java
private void run() {
    
        // Start services.
        try {
    
            t.traceBegin("StartServices");
            startBootstrapServices(t);
            startCoreServices(t);
            startOtherServices(t);
        } catch (Throwable ex) {
    
            Slog.e("System", "******************************************");
            Slog.e("System", "************ Failure starting system services", ex);
            throw ex;
        } finally {
    
            t.traceEnd(); // StartServices
        }
}

// SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
    
	// Bring up services needed for UI.
   	if (mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL) {
    
		t.traceBegin("StartInputMethodManagerLifecycle");
		if (InputMethodSystemProperty.MULTI_CLIENT_IME_ENABLED) {
    
			// 多客户端(针对多屏情况下的一个Sample,默认不启用)
			mSystemServiceManager.startService(
				MultiClientInputMethodManagerService.Lifecycle.class);
         } else {
    
			mSystemServiceManager.startService(InputMethodManagerService.Lifecycle.class);
        }
       t.traceEnd();
     }
}
  • 执行InputMethodManagerServiceLifecycle的构造函数,初始化IMMS。
// InputMethodManagerService.java
public static final class Lifecycle extends SystemService {
    
	private InputMethodManagerService mService;

	public Lifecycle(Context context) {
    
		super(context);
		mService = new InputMethodManagerService(context);
	}

	@Override
	public void onStart() {
    
		// 填加到本地服务
		LocalServices.addService(InputMethodManagerInternal.class,
				new LocalServiceImpl(mService));
		// push到binder service中,之后可以通过bind服务找到IMMS。
		publishBinderService(Context.INPUT_METHOD_SERVICE, mService);
	}
}

//InputMethodManagerService.java
public InputMethodManagerService(Context context) {
    
	mIPackageManager = AppGlobals.getPackageManager();
	mContext = context;
	mRes = context.getResources();
	mHandler = new Handler(this);
	// Note: SettingsObserver doesn't register observers in its constructor.
	// 监听输入法的设置,比如默认输入法
	mSettingsObserver = new SettingsObserver(mHandler);
	// 下面几行获取了相关服务的LocalService对象,IMMS与window、package、input进行交互。比如显示输入法时,需要利用WMS服务判定IME显示层级。
	mIWindowManager = IWindowManager.Stub.asInterface(
			ServiceManager.getService(Context.WINDOW_SERVICE));
	mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
	mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
	mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
	mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
	// 这个写法比较特殊,是一个lambda表达式
	mImeDisplayValidator = displayId -> mWindowManagerInternal.shouldShowIme(displayId);
	mCaller = new HandlerCaller(context, null, new HandlerCaller.Callback() {
    
		@Override
		public void executeMessage(Message msg) {
    
			handleMessage(msg);
		}
	}, true /*asyncHandler*/);
	mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
	mUserManager = mContext.getSystemService(UserManager.class);
	mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
	mHardKeyboardListener = new HardKeyboardListener();
	mHasFeature = context.getPackageManager().hasSystemFeature(
			PackageManager.FEATURE_INPUT_METHODS);
	mSlotIme = mContext.getString(com.android.internal.R.string.status_bar_ime);
	// 判断是否为低内存模式
	mIsLowRam = ActivityManager.isLowRamDeviceStatic();

	Bundle extras = new Bundle();
	extras.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, true);
	@ColorInt final int accentColor = mContext.getColor(
			com.android.internal.R.color.system_notification_accent_color);
	mImeSwitcherNotification =
			new Notification.Builder(mContext, SystemNotificationChannels.VIRTUAL_KEYBOARD)
					.setSmallIcon(com.android.internal.R.drawable.ic_notification_ime_default)
					.setWhen(0)
					.setOngoing(true)
					.addExtras(extras)
					.setCategory(Notification.CATEGORY_SYSTEM)
					.setColor(accentColor);

	Intent intent = new Intent(ACTION_SHOW_INPUT_METHOD_PICKER)
			.setPackage(mContext.getPackageName());
	mImeSwitchPendingIntent = PendingIntent.getBroadcast(mContext, 0, intent,
			PendingIntent.FLAG_IMMUTABLE);

	mShowOngoingImeSwitcherForPhones = false;

	mNotificationShown = false;
	int userId = 0;
	try {
    
		userId = ActivityManager.getService().getCurrentUser().id;
	} catch (RemoteException e) {
    
		Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e);
	}

	mLastSwitchUserId = userId;

	// mSettings should be created before buildInputMethodListLocked
	mSettings = new InputMethodSettings(
			mRes, context.getContentResolver(), mMethodMap, userId, !mSystemReady);

	updateCurrentProfileIds();
	AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId);
	mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked(
			mSettings, context);
}
  • 上述代码中IMMS获取了许多其他服务的代理对象(WindowManager、PackageManager、InputManager等等),通过它们获取相关功能。从这里也可以看出,合理的功能模块划分,是有利于代码的开发维护。
IMM的初始化
  • IMM是一个单例类,在每个应用中有一个实例。应用通过IMM请求IMMS启动输入法,IMMS通过Callback形式通知到IMM,进而告知应用相关输入法状态。
    在这里插入图片描述
  • 添加Window时会实例化ViewRootImpl,在ViewRootImpl中会初始化IMM。
// ViewRootImpl.java
public ViewRootImpl(Context context, Display display) {
    
	this(context, display, WindowManagerGlobal.getWindowSession(),
			false /* useSfChoreographer */);
}

// WindowManagerGlobal.java
public static IWindowSession getWindowSession() {
    
	synchronized (WindowManagerGlobal.class) {
    
		if (sWindowSession == null) {
    
			try {
    
				// Emulate the legacy behavior.  The global instance of InputMethodManager
				// was instantiated here.
				// TODO(b/116157766): Remove this hack after cleaning up @UnsupportedAppUsage
				InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
				IWindowManager windowManager = getWindowManagerService();
				sWindowSession = windowManager.openSession(
						new IWindowSessionCallback.Stub() {
    
							@Override
							public void onAnimatorScaleChanged(float scale) {
    
								ValueAnimator.setDurationScale(scale);
							}
						});
			} catch (RemoteException e) {
    
				throw e.rethrowFromSystemServer();
			}
		}
		return sWindowSession;
	}
}
  • 上面的代码调用了 InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary(),在这个函数中对IMM进行了初始化。
// InputMethodManager.java
public static void ensureDefaultInstanceForDefaultDisplayIfNecessary() {
    
	forContextInternal(Display.DEFAULT_DISPLAY, Looper.getMainLooper());
}

// InputMethodManager.java
private static InputMethodManager forContextInternal(int displayId, Looper looper) {
    
	final boolean isDefaultDisplay = displayId == Display.DEFAULT_DISPLAY;
	synchronized (sLock) {
    
		// 从缓存map中根据displayID查找 imm,如果已经创建则返回。
		InputMethodManager instance = sInstanceMap.get(displayId);
		if (instance != null) {
    
			return instance;
		}
		
		// 创建IMM实例
		instance = createInstance(displayId, looper);
		// For backward compatibility, store the instance also to sInstance for default display.
		if (sInstance == null && isDefaultDisplay) {
    
			sInstance = instance;
		}
		
		// IMM实例放入缓存map
		sInstanceMap.put(displayId, instance);
		return instance;
	}
}

// InputMethodManager.java
private static InputMethodManager createRealInstance(int displayId, Looper looper) {
    
	final IInputMethodManager service;
	try {
    
		// 取得IMMS服务对象。这里个INPUT_METHOD_SERVICE,就是IMMS初始化时push到binder中的service标志。
		service = IInputMethodManager.Stub.asInterface(
				ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE));
	} catch (ServiceNotFoundException e) {
    
		throw new IllegalStateException(e);
	}
	
	// 创建IMM实例对象
	final InputMethodManager imm = new InputMethodManager(service, displayId, looper);
	final long identity = Binder.clearCallingIdentity();
	try {
    
		// 将Client告知IMMS。IMMS内部会管理多个Client(每个应用都会有一个Client)
		service.addClient(imm.mClient, imm.mIInputContext, displayId);
	} catch (RemoteException e) {
    
		e.rethrowFromSystemServer();
	} finally {
    
		Binder.restoreCallingIdentity(identity);
	}
	return imm;
}
  • 到此创建了IMM对象,并获取了与IMMS服务交互的代理对象。每个IMM通过IMMS的addClient将自己的相关信息告诉IMMS,包括 mClient、mIInputContext、displayId。对于DisplayID,就是屏幕的逻辑ID。那么其他两个是什么?
// InputMethodManager.java
private InputMethodManager(IInputMethodManager service, int displayId, Looper looper) {
    
	mService = service;
	mMainLooper = looper;
	mH = new H(looper);
	mDisplayId = displayId;
	// mIInputContext 实际上是IInputContext.Stub对象,输入法上下文。 这个对象会同过 IMMS 最终告知 IMS。通过这个对象,应用端接收输入的相关字符,让view进行处理。
	mIInputContext = new ControlledInputConnectionWrapper(looper, mDummyInputConnection, this,
			null);
}

// InputMethodManager.java
private static class ControlledInputConnectionWrapper extends IInputConnectionWrapper {
    }

// InputMethodManager.java
public abstract class IInputConnectionWrapper extends IInputContext.Stub {
    }

// InputMethodManager.java
// mClient实际上是IInputMethodClient.Stub对象,它作为Callback从IMMS获得输入法相关状态,使得应用可以做出相关动作。
final IInputMethodClient.Stub mClient = new IInputMethodClient.Stub() {
    }

// IInputMethodClient.aidl
/**
 * Interface a client of the IInputMethodManager implements, to identify
 * itself and receive information about changes to the global manager state.
 */
oneway interface IInputMethodClient {
    

// IInputContext.aidl
/**
 * Interface from an input method to the application, allowing it to perform
 * edits on the current input field and other interactions with the application.
 * {@hide}
 */
oneway interface IInputContext {
    
}
IMS的初始化
  • IMS运行在输入法进程中,是一个Application里的service。可以通过BindService获取IMS服务对象。如果系统有多款输入法,那么就会有多个IMS(可以通过 ime list -s查看系统当前支持的输入法服务)。以Android原始自带的LatinIME为例。
    在这里插入图片描述

  • AndroidManifest.xml中定义了Service

<!-- /packages/inputmethods/LatinIME/java/AndroidManifest.xml -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
        coreApp="true"
        package="com.android.inputmethod.latin"
        android:versionCode="28">
    <application android:label="@string/english_ime_name"
            android:icon="@drawable/ic_launcher_keyboard"
            android:supportsRtl="true"
            android:allowBackup="true"
            android:defaultToDeviceProtectedStorage="true"
            android:directBootAware="true">

        <!-- Services -->
        <service android:name="LatinIME"
                android:label="@string/english_ime_name"
                android:permission="android.permission.BIND_INPUT_METHOD">
            <intent-filter>
                <action android:name="android.view.InputMethod" />
            </intent-filter>
            <meta-data android:name="android.view.im" android:resource="@xml/method" />
        </service>		
		
    </application>
</manifest>
  • LatinIME的实现类继承了InputMethodService,也就是实现了IMS。
/**
 * Input method implementation for Qwerty'ish keyboard.
 */
public class LatinIME extends InputMethodService implements KeyboardActionListener,
        SuggestionStripView.Listener, SuggestionStripViewAccessor,
        DictionaryFacilitator.DictionaryInitializationListener,
        PermissionsManager.PermissionsResultCallback {
     }
  • 点击文本输入框触发Focus焦点变更是,IMM会告知IMMS启动IMS(这个流程在下章会介绍,这个关注IMS自身的初始化。),IMMS通过BindServic初始化IMS服务。
///packages/inputmethods/LatinIME/java/src/com/android/inputmethod/latin/LatinIME.java
public void onCreate() {
    
	// LatinIME会进行自身的一些初始化,这里主要关注其InputMethodService的初始化。
	super.onCreate();
}

// InputMethodService.java
@Override public void onCreate() {
    
	mTheme = Resources.selectSystemTheme(mTheme,
			getApplicationInfo().targetSdkVersion,
			android.R.style.Theme_InputMethod,
			android.R.style.Theme_Holo_InputMethod,
			android.R.style.Theme_DeviceDefault_InputMethod,
			android.R.style.Theme_DeviceDefault_InputMethod);
	super.setTheme(mTheme);
	super.onCreate();
	// 获取IMMS服务对象
	mImm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
	mSettingsObserver = SettingsObserver.createAndRegister(this);

	// 判断是否为车载系统
	mIsAutomotive = isAutomotive();
	mAutomotiveHideNavBarForKeyboard = getApplicationContext().getResources().getBoolean(
			com.android.internal.R.bool.config_automotiveHideNavBarForKeyboard);

	// TODO(b/111364446) Need to address context lifecycle issue if need to re-create
	// for update resources & configuration correctly when show soft input
	// in non-default display.
	mInflater = (LayoutInflater)getSystemService(
			Context.LAYOUT_INFLATER_SERVICE);
			
	// 创建输入法窗口(Dialog类型)
	mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
			WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);
	mWindow.getWindow().getAttributes().setFitInsetsTypes(statusBars() | navigationBars());
	mWindow.getWindow().getAttributes().setFitInsetsSides(Side.all() & ~Side.BOTTOM);
	mWindow.getWindow().getAttributes().setFitInsetsIgnoringVisibility(true);

	// IME layout should always be inset by navigation bar, no matter its current visibility,
	// unless automotive requests it. Automotive devices may request the navigation bar to be
	// hidden when the IME shows up (controlled via config_automotiveHideNavBarForKeyboard)
	// in order to maximize the visible screen real estate. When this happens, the IME window
	// should animate from the bottom of the screen to reduce the jank that happens from the
	// lack of synchronization between the bottom system window and the IME window.
	if (mIsAutomotive && mAutomotiveHideNavBarForKeyboard) {
    
		mWindow.getWindow().setDecorFitsSystemWindows(false);
	}
	mWindow.getWindow().getDecorView().setOnApplyWindowInsetsListener(
			(v, insets) -> v.onApplyWindowInsets(
					new WindowInsets.Builder(insets).setInsets(
							navigationBars(),
							insets.getInsetsIgnoringVisibility(navigationBars()))
							.build()));

	// For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set
	// by default (but IME developers can opt this out later if they want a new behavior).
	mWindow.getWindow().setFlags(
			FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
	// 初始化View相关内容
	initViews();
	mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);

	mInlineSuggestionSessionController = new InlineSuggestionSessionController(
			this::onCreateInlineSuggestionsRequest, this::getHostInputToken,
			this::onInlineSuggestionsResponse);
}

// SoftInputWindow.java
public SoftInputWindow(Context context, String name, int theme, Callback callback,
		KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState,
		int windowType, int gravity, boolean takesFocus) {
    
	super(context, theme);
	mName = name;
	mCallback = callback;
	mKeyEventCallback = keyEventCallback;
	mDispatcherState = dispatcherState;
	mWindowType = windowType;
	mGravity = gravity;
	mTakesFocus = takesFocus;
	initDockWindow();
}

// SoftInputWindow.java
private void initDockWindow() {
    
	WindowManager.LayoutParams lp = getWindow().getAttributes();
	// mWindowType是 WindowManager.LayoutParams.TYPE_INPUT_METHOD,可以通过改这里改变输入法WindowType,进行影响默认层级。
	lp.type = mWindowType;
	lp.setTitle(mName);

	lp.gravity = mGravity;
	updateWidthHeight(lp);

	getWindow().setAttributes(lp);

	int windowSetFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
	int windowModFlags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
			WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
			WindowManager.LayoutParams.FLAG_DIM_BEHIND;
    // 默认走if里面,不获取焦点。
	if (!mTakesFocus) {
    
		windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
	} else {
    
		windowSetFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
		windowModFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
	}

	getWindow().setFlags(windowSetFlags, windowModFlags);
}
  • 上面对于IMS进行了一些初始化,主要是设置输入法窗口的一些属性。下面看一下,IMS通过onBind接口返回的Binder对象。Client端通过onBind时返回的对象与IMS服务交互。IMS继承了AbstractInputMethodService,onBind的 实现定义在这个类中。
// AbstractInputMethodService.java
public abstract class AbstractInputMethodService extends Service {
    
    final public IBinder onBind(Intent intent) {
    
        if (mInputMethod == null) {
    
            mInputMethod = onCreateInputMethodInterface();
        }
        // IMMS通过这个对象控制 输入法服务(IMS)。IInputMethodWrapper 实际上是IInputMethod.Stub类型。
        return new IInputMethodWrapper(this, mInputMethod);
    }
}

// IInputMethodWrapper.java
class IInputMethodWrapper extends IInputMethod.Stub {
    }

// IInputMethod.aidl
oneway interface IInputMethod {
    }
  • 综上,IMS启动完成。返回 IInputMethod.stub对象给IMMS用于操作IMS。
输入法的启动
  • 上面的内容,主要关注 IMM、IMS、IMMS的初始化过程。在应用中点击文本输入框会弹出输入法界面。下面主要对这个流程进行分析。
    在这里插入图片描述

  • 点击文本输入框后,控件获取焦点,会触发ViewRootImpl的焦点变更流程。这个流程会调用IMM的startInput函数启动输入法。

// ViewRootImpl.java
public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
    
	Message msg = Message.obtain();
	msg.what = MSG_WINDOW_FOCUS_CHANGED;
	mHandler.sendMessage(msg);
}
// ViewRootImpl.java
public void handleMessage(Message msg) 
	// 省略
	case MSG_WINDOW_FOCUS_CHANGED: {
    
	handleWindowFocusChanged();
	} break;
}

// ViewRootImpl.java
private void handleWindowFocusChanged() {
    
	if (mAdded) {
    
		// Note: must be done after the focus change callbacks,
		// so all of the view state is set up correctly.
		mImeFocusController.onPostWindowFocus(mView.findFocus(), hasWindowFocus,
				mWindowAttributes);
	}
}

// ImeFocusController.java
void onPostWindowFocus(View focusedView, boolean hasWindowFocus,
		WindowManager.LayoutParams windowAttribute) {
    
	// 没有焦点的话,不弹出输入法
	if (!hasWindowFocus || !mHasImeFocus || isInLocalFocusMode(windowAttribute)) {
    
		return;
	}

	// 获取Delegate对象(包装了IMM)
	boolean forceFocus = false;
	final InputMethodManagerDelegate immDelegate = getImmDelegate();

	// 请求启动输入法
	immDelegate.startInputAsyncOnWindowFocusGain(viewForWindowFocus,
			windowAttribute.softInputMode, windowAttribute.flags, forceFocus);
}

// InputMethodManager.java
public void startInputAsyncOnWindowFocusGain(View focusedView,
		@SoftInputModeFlags int softInputMode, int windowFlags, boolean forceNewFocus) {
    
	if (controller.checkFocus(forceNewFocus, false)) {
    
		// We need to restart input on the current focus view.  This
		// should be done in conjunction with telling the system service
		// about the window gaining focus, to help make the transition
		// smooth.
        //  通常情况下会走到这里
		if (startInput(StartInputReason.WINDOW_FOCUS_GAIN,
				focusedView, startInputFlags, softInputMode, windowFlags)) {
    
			return;
		}
	}
}

// InputMethodManager.java
public boolean startInput(@StartInputReason int startInputReason, View focusedView,
		@StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
		int windowFlags) {
    

	// 这些代码是在UIThread中执行的
	return startInputInner(startInputReason,
			focusedView != null ? focusedView.getWindowToken() : null, startInputFlags,
			softInputMode, windowFlags);
}

// InputMethodManager.java
boolean startInputInner(@StartInputReason int startInputReason,
		@Nullable IBinder windowGainingFocus, @StartInputFlags int startInputFlags,
		@SoftInputModeFlags int softInputMode, int windowFlags) {
    
	final View view;
	synchronized (mH) {
    
		view = getServedViewLocked();
	}

	// Okay we are now ready to call into the served view and have it
	// do its stuff.
	// Life is good: let's hook everything up!
	// 记录编辑器相关信息的对象,输入法根据这些信息显示不同的效果
	EditorInfo tba = new EditorInfo();

	tba.packageName = view.getContext().getOpPackageName();
	tba.autofillId = view.getAutofillId();
	tba.fieldId = view.getId();
	// 创建InputConnection,调用的是TextView中的对应函数。创建了EditableInputConnection类型对象
	// 后续利用InputConnection对目标控件进行相关字符串操作
	InputConnection ic = view.onCreateInputConnection(tba);

	synchronized (mH) {
    
	 
		if (ic != null) {
    
			// 这个对象实际上是 IInputContext.stub对象。上面创建的InpuConnection传给这个对象。
			// IMS与 IInputContext.stub交互, IInputContext.stub通过 InpuConnection与控件交互。
			servedContext = new ControlledInputConnectionWrapper(
					icHandler != null ? icHandler.getLooper() : vh.getLooper(), ic, this, view);
		} else {
    
			servedContext = null;
			missingMethodFlags = 0;
		}
		mServedInputConnectionWrapper = servedContext;

		try {
    
			// 真正启动输入法的地方,返回的InputBindResult是一个Parcelable
			final InputBindResult res = mService.startInputOrWindowGainedFocus(
					startInputReason, mClient, windowGainingFocus, startInputFlags,
					softInputMode, windowFlags, tba, servedContext, missingMethodFlags,
					view.getContext().getApplicationInfo().targetSdkVersion);

			if (res == null) {
    
				Log.wtf(TAG, "startInputOrWindowGainedFocus must not return"
						+ " null. startInputReason="
						+ InputMethodDebug.startInputReasonToString(startInputReason)
						+ " editorInfo=" + tba
						+ " startInputFlags="
						+ InputMethodDebug.startInputFlagsToString(startInputFlags));
				return false;
			}
			
			if (res.id != null) {
    
				// 设置InputChannel
				setInputChannelLocked(res.channel);
				mBindSequence = res.sequence;
				// IInputMethodSession类型对象,这个对象是IMS的Binder代理。通过它与IMS直接交互。
				// 这样应用端就拿到了与IMS直接交互的对象
				mCurMethod = res.method;
				// 当前输入法的ID(不同输入法ID值不一样)
				mCurId = res.id;
			} else if (res.channel != null && res.channel != mCurChannel) {
    
				res.channel.dispose();
			}

		} catch (RemoteException e) {
    
			Log.w(TAG, "IME died: " + mCurId, e);
		}
	}

	return true;
}
  • 如果startInputInner执行成功的话,应用端的IMM中便会持有 IInputMethodSession类型对象,通过它与IMS进行交互。上面的mService是IMMS的客户端代理,在其startInputOrWindowGainedFocus函数会启动输入法。
public InputBindResult startInputOrWindowGainedFocus(
		@StartInputReason int startInputReason, IInputMethodClient client, IBinder windowToken,
		@StartInputFlags int startInputFlags, @SoftInputModeFlags int softInputMode,
		int windowFlags, @Nullable EditorInfo attribute, IInputContext inputContext,
		@MissingMethodFlags int missingMethods, int unverifiedTargetSdkVersion) {
    


	final InputBindResult result;
	synchronized (mMethodMap) {
    
		final long ident = Binder.clearCallingIdentity();
		try {
    
			// 加锁调用
			result = startInputOrWindowGainedFocusInternalLocked(startInputReason, client,
					windowToken, startInputFlags, softInputMode, windowFlags, attribute,
					inputContext, missingMethods, unverifiedTargetSdkVersion, userId);
		} finally {
    
			Binder.restoreCallingIdentity(ident);
		}
	}

	return result;
}

// InputMethodManagerService.java
private InputBindResult startInputOrWindowGainedFocusInternalLocked(
		@StartInputReason int startInputReason, IInputMethodClient client,
		@NonNull IBinder windowToken, @StartInputFlags int startInputFlags,
		@SoftInputModeFlags int softInputMode, int windowFlags, EditorInfo attribute,
		IInputContext inputContext, @MissingMethodFlags int missingMethods,
		int unverifiedTargetSdkVersion, @UserIdInt int userId) {
    

	// 计算IME的TargeWindow,输入法窗口会根据TargetWindow动态计算显示层级
	// 此函数会调用到WMS,并调用到DisplayContent::computeImeTarget函数中。
	if (!mWindowManagerInternal.isInputMethodClientFocus(cs.uid, cs.pid,
			cs.selfReportedDisplayId)) {
    
		// Check with the window manager to make sure this client actually
		// has a window with focus.  If not, reject.  This is thread safe
		// because if the focus changes some time before or after, the
		// next client receiving focus that has any interest in input will
		// be calling through here after that change happens.
		if (DEBUG) {
    
			Slog.w(TAG, "Focus gain on non-focused client " + cs.client
					+ " (uid=" + cs.uid + " pid=" + cs.pid + ")");
		}
		return InputBindResult.NOT_IME_TARGET_WINDOW;
	}
	
	// 判断是否是相同的Window获得了Focus
	final boolean sameWindowFocused = mCurFocusedWindow == windowToken;
	// 判断是不是文本编辑器
	final boolean isTextEditor = (startInputFlags & StartInputFlags.IS_TEXT_EDITOR) != 0;
	// 启动要因是否为得到焦点
	final boolean startInputByWinGainedFocus =
			(startInputFlags & StartInputFlags.WINDOW_GAINED_FOCUS) != 0;
	
	// 如果焦点window一样,并且是本文编辑器。表示之前已经启动了输入法,直接启动。
	if (sameWindowFocused && isTextEditor) {
    
		if (DEBUG) {
    
			Slog.w(TAG, "Window already focused, ignoring focus gain of: " + client
					+ " attribute=" + attribute + ", token = " + windowToken
					+ ", startInputReason="
					+ InputMethodDebug.startInputReasonToString(startInputReason));
		}
		if (attribute != null) {
    
			return startInputUncheckedLocked(cs, inputContext, missingMethods,
					attribute, startInputFlags, startInputReason);
		}
		return new InputBindResult(
				InputBindResult.ResultCode.SUCCESS_REPORT_WINDOW_FOCUS_ONLY,
				null, null, null, -1, null);
	}


	// We want to start input before showing the IME, but after closing
	// it.  We want to do this after closing it to help the IME disappear
	// more quickly (not get stuck behind it initializing itself for the
	// new focused input, even if its window wants to hide the IME).
	boolean didStart = false;
	// 判断android:windowSoftInputMode 
	InputBindResult res = null;
	switch (softInputMode & LayoutParams.SOFT_INPUT_MASK_STATE) {
    
		// 默认情况下走这里
		case LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED:
			if (!sameWindowFocused && (!isTextEditor || !doAutoShow)) {
    
				if (LayoutParams.mayUseInputMethod(windowFlags)) {
    
					// There is no focus view, and this window will
					// be behind any soft input window, so hide the
					// soft input window if it is shown.
					if (DEBUG) Slog.v(TAG, "Unspecified window will hide input");
					hideCurrentInputLocked(
							mCurFocusedWindow, InputMethodManager.HIDE_NOT_ALWAYS, null,
							SoftInputShowHideReason.HIDE_UNSPECIFIED_WINDOW);

					// If focused display changed, we should unbind current method
					// to make app window in previous display relayout after Ime
					// window token removed.
					// Note that we can trust client's display ID as long as it matches
					// to the display ID obtained from the window.
					if (cs.selfReportedDisplayId != mCurTokenDisplayId) {
    
						unbindCurrentMethodLocked();
					}
				}
			} else if (isTextEditor && doAutoShow
					&& (softInputMode & LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != 0) {
    
				// There is a focus view, and we are navigating forward
				// into the window, so show the input window for the user.
				// We only do this automatically if the window can resize
				// to accommodate the IME (so what the user sees will give
				// them good context without input information being obscured
				// by the IME) or if running on a large screen where there
				// is more room for the target window + IME.
				if (DEBUG) Slog.v(TAG, "Unspecified window will show input");
				if (attribute != null) {
    
					// 启动输入法
					res = startInputUncheckedLocked(cs, inputContext, missingMethods,
							attribute, startInputFlags, startInputReason);
					didStart = true;
				}
				// 显示输入法
				showCurrentInputLocked(windowToken, InputMethodManager.SHOW_IMPLICIT, null,
						SoftInputShowHideReason.SHOW_AUTO_EDITOR_FORWARD_NAV);
			}
			break;
		case LayoutParams.SOFT_INPUT_STATE_UNCHANGED:
			//  后面的代码省略。遇到问题时,可以根据具体情况,加log分析。
	}

	if (!didStart) {
    
		// 如果没有启动的话,这里会做一下保护。感兴趣的可以看源码研究一下。
	}
	return res;
}

// InputMethodManagerService.java
InputBindResult startInputUncheckedLocked(@NonNull ClientState cs, IInputContext inputContext,
		@MissingMethodFlags int missingMethods, @NonNull EditorInfo attribute,
		@StartInputFlags int startInputFlags, @StartInputReason int startInputReason) {
    
	// If no method is currently selected, do nothing.
	// 如果当前没有输入法,直接返回
	if (mCurMethodId == null) {
    
		return InputBindResult.NO_IME;
	}

	// 启动没有ready,直接返回
	if (!mSystemReady) {
    
		// If the system is not yet ready, we shouldn't be running third
		// party code.
		return new InputBindResult(
				InputBindResult.ResultCode.ERROR_SYSTEM_NOT_READY,
				null, null, mCurMethodId, mCurSeq, null);
	}

	// 得到显示输入法的DisplayID
	final int displayIdToShowIme = computeImeDisplayIdForTarget(cs.selfReportedDisplayId,
			mImeDisplayValidator);

	// Check if the input method is changing.
	// We expect the caller has already verified that the client is allowed to access this
	// display ID.
	// 走到这个判断里面,基本上就是已经绑定过输入法了。直接返回结果就行。
	if (mCurId != null && mCurId.equals(mCurMethodId)
			&& displayIdToShowIme == mCurTokenDisplayId) {
    
		
	}

	// 没有绑定过,则重新开发绑定输入法。
	InputMethodInfo info = mMethodMap.get(mCurMethodId);
	if (info == null) {
    
		throw new IllegalArgumentException("Unknown id: " + mCurMethodId);
	}

	unbindCurrentMethodLocked();

	mCurIntent = new Intent(InputMethod.SERVICE_INTERFACE);
	mCurIntent.setComponent(info.getComponent());
	mCurIntent.putExtra(Intent.EXTRA_CLIENT_LABEL,
			com.android.internal.R.string.input_method_binding_label);
	mCurIntent.putExtra(Intent.EXTRA_CLIENT_INTENT, PendingIntent.getActivity(
			mContext, 0, new Intent(Settings.ACTION_INPUT_METHOD_SETTINGS),
			PendingIntent.FLAG_IMMUTABLE));
	// 实际上是调用BindService,获取输入法服务(IMS)
	if (bindCurrentInputMethodServiceLocked(mCurIntent, this, IME_CONNECTION_BIND_FLAGS)) {
    
		mLastBindTime = SystemClock.uptimeMillis();
		mHaveConnection = true;
		mCurId = info.getId();
		mCurToken = new Binder();
		mCurTokenDisplayId = displayIdToShowIme;
		try {
    
			if (DEBUG) {
    
				Slog.v(TAG, "Adding window token: " + mCurToken + " for display: "
						+ mCurTokenDisplayId);
			}
			
			// 添加用于显示输入法的Token
			mIWindowManager.addWindowToken(mCurToken, LayoutParams.TYPE_INPUT_METHOD,
					mCurTokenDisplayId);
		} catch (RemoteException e) {
    
		}
		// 成功:返回正在等待绑定IMS
		return new InputBindResult(
				InputBindResult.ResultCode.SUCCESS_WAITING_IME_BINDING,
				null, null, mCurId, mCurSeq, null);
	}
	
	mCurIntent = null;
	Slog.w(TAG, "Failure connecting to input method service: " + mCurIntent);
	return InputBindResult.IME_NOT_CONNECTED;
}

// InputMethodManagerService.java
// BindService成功后的回调
public void onServiceConnected(ComponentName name, IBinder service) {
    
	synchronized (mMethodMap) {
    
		if (mCurIntent != null && name.equals(mCurIntent.getComponent())) {
    
			//得到IInputMethod对象,IMMS通过这个对象与IMS交互。
			mCurMethod = IInputMethod.Stub.asInterface(service);
			
			//初始化输入法
			executeOrSendMessage(mCurMethod, mCaller.obtainMessageIOO(
					MSG_INITIALIZE_IME, mCurTokenDisplayId, mCurMethod, mCurToken));
			scheduleNotifyImeUidToAudioService(mCurMethodUid);
			if (mCurClient != null) {
    
				// 接上述流程,此时有客户端等待。先清理session,然后创建session。session用于应用与输入法交互。
				clearClientSessionLocked(mCurClient);
				requestClientSessionLocked(mCurClient);
			}
		}
	}
}

// InputMethodManagerService.java
void requestClientSessionLocked(ClientState cs) {
    
	if (!cs.sessionRequested) {
    
		if (DEBUG) Slog.v(TAG, "Creating new session for client " + cs);
		InputChannel[] channels = InputChannel.openInputChannelPair(cs.toString());
		cs.sessionRequested = true;
		executeOrSendMessage(mCurMethod, mCaller.obtainMessageOOO(
				MSG_CREATE_SESSION, mCurMethod, channels[1],
				new MethodCallback(this, mCurMethod, channels[0])));
	}
}


// IInputMethodWrapper.java
public void createSession(InputChannel channel, IInputSessionCallback callback) {
    
	mCaller.executeOrSendMessage(mCaller.obtainMessageOO(DO_CREATE_SESSION,
			channel, callback));
}
// IInputMethodWrapper.java
public void executeMessage(Message msg) {
    
	case DO_CREATE_SESSION: {
    
		SomeArgs args = (SomeArgs)msg.obj;
		inputMethod.createSession(new InputMethodSessionCallbackWrapper(
				mContext, (InputChannel)args.arg1,
				(IInputSessionCallback)args.arg2));
		args.recycle();
		return;
	}
}

// AbstractInputMethodService.java
public abstract class AbstractInputMethodImpl implements InputMethod {
    
	/**
	 * Instantiate a new client session for the input method, by calling
	 * back to {@link AbstractInputMethodService#onCreateInputMethodSessionInterface()
	 * AbstractInputMethodService.onCreateInputMethodSessionInterface()}.
	 */
	@MainThread
	public void createSession(SessionCallback callback) {
    
		// 走这里,把session通知回去(IMS给IMMS通知)
		callback.sessionCreated(onCreateInputMethodSessionInterface());
	}
}

// InputMethodService.java
// InputMethodSessionImpl 这个对象,在IInputMethodWrapper.java中被 被InputMethodSessionCallbackWrapper包装成 IInputMethodSessionWrapper 对象。
// IInputMethodSessionWrapper 是IInputMethodSession.Stub 类型。
public AbstractInputMethodSessionImpl onCreateInputMethodSessionInterface() {
    
     return new InputMethodSessionImpl();
 }
  • IMS创建了将IInputMethodSession的代理,并通过Callback返回给IMMS。
// InputMethodManagerService.java
// callback.sessionCreated 通过Binder回调到IMMS端的这个函数。
void onSessionCreated(IInputMethod method, IInputMethodSession session,
		InputChannel channel) {
    
	synchronized (mMethodMap) {
    
	
		if (mCurMethod != null && method != null
				&& mCurMethod.asBinder() == method.asBinder()) {
    
			if (mCurClient != null) {
    
				clearClientSessionLocked(mCurClient);
				// 这个Client是IMM 通过addClient告知 IMMS的。它对应着某个应用端
				mCurClient.curSession = new SessionState(mCurClient,
						method, session, channel);
                // 可以真正启动输入法了!!!
				InputBindResult res = attachNewInputLocked(
						StartInputReason.SESSION_CREATED_BY_IME, true);
				if (res.method != null) {
    
					// method 是 InputSession。如果非空,代表IMS已经创建了一个会话,那么 将这个会话与对应的应用Client端绑定。实际上调用了IInputMethodClient 的onBindMethod,将Parcelabled对象告知应用端。
					executeOrSendMessage(mCurClient.client, mCaller.obtainMessageOO(
							MSG_BIND_CLIENT, mCurClient.client, res));
				}
				return;
			}
		}
	}
}

// InputMethodManagerService.java
InputBindResult attachNewInputLocked(@StartInputReason int startInputReason, boolean initial) {
    
	if (!mBoundToMethod) {
    
		// 将客户端绑定到IME(IMS),将InputConnection告知IMS。
		// 调用InputMethod的bindInput API
		executeOrSendMessage(mCurMethod, mCaller.obtainMessageOO(
				MSG_BIND_INPUT, mCurMethod, mCurClient.binding));
		mBoundToMethod = true;
	}


	// 启动输入法(告知IMS显示输入法)
	final SessionState session = mCurClient.curSession;
	executeOrSendMessage(session.method, mCaller.obtainMessageIIOOOO(
			MSG_START_INPUT, mCurInputContextMissingMethods, initial ? 0 : 1 /* restarting */,
			startInputToken, session, mCurInputContext, mCurAttribute));
	if (mShowRequested) {
    
		// 显示输入法,调用了InputMethod的showSoftInput
		if (DEBUG) Slog.v(TAG, "Attach new input asks to show input");
		showCurrentInputLocked(mCurFocusedWindow, getAppShowFlags(), null,
				SoftInputShowHideReason.ATTACH_NEW_INPUT);
	}
	return new InputBindResult(InputBindResult.ResultCode.SUCCESS_WITH_IME_SESSION,
			session.session, (session.channel != null ? session.channel.dup() : null),
			mCurId, mCurSeq, mCurActivityViewToScreenMatrix);
}

// InputMethodManagerService.java
public boolean handleMessage(Message msg) {
    
	case MSG_START_INPUT: {
    
		final int missingMethods = msg.arg1;
		final boolean restarting = msg.arg2 != 0;
		args = (SomeArgs) msg.obj;
		final IBinder startInputToken = (IBinder) args.arg1;
		final SessionState session = (SessionState) args.arg2;
		final IInputContext inputContext = (IInputContext) args.arg3;
		final EditorInfo editorInfo = (EditorInfo) args.arg4;
		try {
    
			setEnabledSessionInMainThread(session);
			session.method.startInput(startInputToken, inputContext, missingMethods,
					editorInfo, restarting, session.client.shouldPreRenderIme);
		} catch (RemoteException e) {
    
		}
		args.recycle();
		return true;
	}
}
  • 到这里,输入法启动的大部分流程已经完成。当客户端的 onBindMethod被触发(InputMethodManager.java)应用客户端就收到了输入法对象,后续做了绑定以及再次请求启动输入法(此时已经启动过了)等操作。这些操作,遇到相关问题 时看代码分析即可。
输入法组件图
  • 综上,总结一下IMS、IMM和IMMS的组件图。通过组件图可以了解个模块间的交互接口。
  1. IInputMethodManager: IMM通过它请求IMMS
  2. IInputMethodClient: IMMS通过它告知IMM相关通知及状态(包括Session对象)
  3. IInputMethod: IMMS用来请求IMS的对象
  4. IInputMethodSessionCallback: IMS通过这个Callback,把
  5. IInputMethodSession告知IMMS,进而告知IMM
  6. InputContext:IMM通过IMMS告知IMS的对象,IMS通过这个对象回调IMM
  7. IInputMethodSession:IMM用来请求IMS的对象
    在这里插入图片描述

输入法调试

  • 可以通过一下方式配置系统输入法(PS:原生Setting中有输入法设置画面,但实际项目中原始Setting一般都会被禁用或只能 以Debug方式启动。)
通过配置文件修改默认输入法
  • 在framework的res文件中,定义def_input_method和config_default_input_method的值,并在DatabaseHelper.java的loadSecureSettings中加载定义的默认值(前提是输入法应用已被打包到系统)
<!-- frameworks/base/packages/SettingsProvider/res/values/defaults.xml -->
<string name="def_input_method" translatable="false">xxxx</string>
<string name="def_enabled_input_methods" translatable="false">xxxxx</string> 
// /frameworks/base/packages/SettingsProvider/src/com/android/providers/settings/DatabaseHelper.java

 private void loadSecureSettings(SQLiteDatabase db) {
    
 	loadStringSetting(stmt, Settings.Secure.ENABLED_INPUT_METHODS,R.string.def_enabled_input_methods);
	loadStringSetting(stmt,Settings.Secure.DEFAULT_INPUT_METHOD,R.string.def_input_method);
 }
通过ime命令调试
  • 通过ime命令,配置当前系统的输入法
# xxx.apk 是输入法安装包
# root和remount非必须命令
adb root
adb remount 
adb install xxx.apk

# 启用输入法,否则ims list -s 看不到输入法
adb shell
# 比如 com.android.inputmethod.leanback/.service.LeanbackImeService
# 根据自己安装的输入法信息设置
# 实在不知道的,可以通过 dumpsys package 包名 | grep Service 确认
ime enable 包名/.Service名
ime set 包名/.Service名

# 点击输入法测试即可

##### 查看输入法相关状态
dumpsys input_method
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zxc024000/article/details/133964507

智能推荐

5个超厉害的资源搜索网站,每一款都可以让你的资源满满!_最全资源搜索引擎-程序员宅基地

文章浏览阅读1.6w次,点赞8次,收藏41次。生活中我们无时不刻不都要在网站搜索资源,但就是缺少一个趁手的资源搜索网站,如果有一个比较好的资源搜索网站可以帮助我们节省一大半时间!今天小编在这里为大家分享5款超厉害的资源搜索网站,每一款都可以让你的资源丰富精彩!网盘传奇一款最有效的网盘资源搜索网站你还在为找网站里面的资源而烦恼找不到什么合适的工具而烦恼吗?这款网站传奇网站汇聚了4853w个资源,并且它每一天都会持续更新资源;..._最全资源搜索引擎

Book类的设计(Java)_6-1 book类的设计java-程序员宅基地

文章浏览阅读4.5k次,点赞5次,收藏18次。阅读测试程序,设计一个Book类。函数接口定义:class Book{}该类有 四个私有属性 分别是 书籍名称、 价格、 作者、 出版年份,以及相应的set 与get方法;该类有一个含有四个参数的构造方法,这四个参数依次是 书籍名称、 价格、 作者、 出版年份 。裁判测试程序样例:import java.util.*;public class Main { public static void main(String[] args) { List <Book>_6-1 book类的设计java

基于微信小程序的校园导航小程序设计与实现_校园导航微信小程序系统的设计与实现-程序员宅基地

文章浏览阅读613次,点赞28次,收藏27次。相比于以前的传统手工管理方式,智能化的管理方式可以大幅降低学校的运营人员成本,实现了校园导航的标准化、制度化、程序化的管理,有效地防止了校园导航的随意管理,提高了信息的处理速度和精确度,能够及时、准确地查询和修正建筑速看等信息。课题主要采用微信小程序、SpringBoot架构技术,前端以小程序页面呈现给学生,结合后台java语言使页面更加完善,后台使用MySQL数据库进行数据存储。微信小程序主要包括学生信息、校园简介、建筑速看、系统信息等功能,从而实现智能化的管理方式,提高工作效率。

有状态和无状态登录

传统上用户登陆状态会以 Session 的形式保存在服务器上,而 Session ID 则保存在前端的 Cookie 中;而使用 JWT 以后,用户的认证信息将会以 Token 的形式保存在前端,服务器不需要保存任何的用户状态,这也就是为什么 JWT 被称为无状态登陆的原因,无状态登陆最大的优势就是完美支持分布式部署,可以使用一个 Token 发送给不同的服务器,而所有的服务器都会返回同样的结果。有状态和无状态最大的区别就是服务端会不会保存客户端的信息。

九大角度全方位对比Android、iOS开发_ios 开发角度-程序员宅基地

文章浏览阅读784次。发表于10小时前| 2674次阅读| 来源TechCrunch| 19 条评论| 作者Jon EvansiOSAndroid应用开发产品编程语言JavaObjective-C摘要:即便Android市场份额已经超过80%,对于开发者来说,使用哪一个平台做开发仍然很难选择。本文从开发环境、配置、UX设计、语言、API、网络、分享、碎片化、发布等九个方面把Android和iOS_ios 开发角度

搜索引擎的发展历史

搜索引擎的发展历史可以追溯到20世纪90年代初,随着互联网的快速发展和信息量的急剧增加,人们开始感受到了获取和管理信息的挑战。这些阶段展示了搜索引擎在技术和商业模式上的不断演进,以满足用户对信息获取的不断增长的需求。

随便推点

控制对象的特性_控制对象特性-程序员宅基地

文章浏览阅读990次。对象特性是指控制对象的输出参数和输入参数之间的相互作用规律。放大系数K描述控制对象特性的静态特性参数。它的意义是:输出量的变化量和输入量的变化量之比。时间常数T当输入量发生变化后,所引起输出量变化的快慢。(动态参数) ..._控制对象特性

FRP搭建内网穿透(亲测有效)_locyanfrp-程序员宅基地

文章浏览阅读5.7w次,点赞50次,收藏276次。FRP搭建内网穿透1.概述:frp可以通过有公网IP的的服务器将内网的主机暴露给互联网,从而实现通过外网能直接访问到内网主机;frp有服务端和客户端,服务端需要装在有公网ip的服务器上,客户端装在内网主机上。2.简单的图解:3.准备工作:1.一个域名(www.test.xyz)2.一台有公网IP的服务器(阿里云、腾讯云等都行)3.一台内网主机4.下载frp,选择适合的版本下载解压如下:我这里服务器端和客户端都放在了/usr/local/frp/目录下4.执行命令# 服务器端给执_locyanfrp

UVA 12534 - Binary Matrix 2 (网络流‘最小费用最大流’ZKW)_uva12534-程序员宅基地

文章浏览阅读687次。题目:http://acm.hust.edu.cn/vjudge/contest/view.action?cid=93745#problem/A题意:给出r*c的01矩阵,可以翻转格子使得0表成1,1变成0,求出最小的步数使得每一行中1的个数相等,每一列中1的个数相等。思路:网络流。容量可以保证每一行和每一列的1的个数相等,费用可以算出最小步数。行向列建边,如果该格子是_uva12534

免费SSL证书_csdn alphassl免费申请-程序员宅基地

文章浏览阅读504次。1、Let's Encrypt 90天,支持泛域名2、Buypass:https://www.buypass.com/ssl/resources/go-ssl-technical-specification6个月,单域名3、AlwaysOnSLL:https://alwaysonssl.com/ 1年,单域名 可参考蜗牛(wn789)4、TrustAsia5、Alpha..._csdn alphassl免费申请

测试算法的性能(以选择排序为例)_算法性能测试-程序员宅基地

文章浏览阅读1.6k次。测试算法的性能 很多时候我们需要对算法的性能进行测试,最简单的方式是看算法在特定的数据集上的执行时间,简单的测试算法性能的函数实现见testSort()。【思想】:用clock_t计算某排序算法所需的时间,(endTime - startTime)/ CLOCKS_PER_SEC来表示执行了多少秒。【关于宏CLOCKS_PER_SEC】:以下摘自百度百科,“CLOCKS_PE_算法性能测试

Lane Detection_lanedetectionlite-程序员宅基地

文章浏览阅读1.2k次。fromhttps://towardsdatascience.com/finding-lane-lines-simple-pipeline-for-lane-detection-d02b62e7572bIdentifying lanes of the road is very common task that human driver performs. This is important ..._lanedetectionlite

推荐文章

热门文章

相关标签