• Keyguard分析


        从Android 6.0开始,位于frameworks/bases/packages/Keyguard的Keyguard开始被编译为一个jar包,被SystemUI静态导入,相当于SystemUI的一个界面,这样Keyguard就可以复用SystemUI里关于通知的那一部分代码,这个在Keyuard的Makefile里可以看到

     1 LOCAL_PATH:= $(call my-dir)
     2 17include $(CLEAR_VARS)
     3 18
     4 19LOCAL_SRC_FILES := $(call all-java-files-under, src) $(call all-subdir-Iaidl-files)
     5 20
     6 21LOCAL_MODULE := Keyguard
     7 22
     8 23LOCAL_CERTIFICATE := platform
     9 24
    10 25LOCAL_JAVA_LIBRARIES := SettingsLib
    11 26
    12 27LOCAL_PRIVILEGED_MODULE := true
    13 28
    14 29LOCAL_PROGUARD_FLAG_FILES := proguard.flags
    15 30
    16 31LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
    17 32
    18 33include $(BUILD_STATIC_JAVA_LIBRARY)
    19 34
    20 35#include $(call all-makefiles-under,$(LOCAL_PATH))

           Keyguard分为两个界面,不用输入密码的一级锁屏界面(SystemUI认为是PhoneStatusBar)与相应源码文件包含Security字样的二级锁屏界面(SystemUI认为是Bouncer)。

           

           一级锁屏界面

           

           二级锁屏界面

           各个部件调用关系是下边这张图

           

           可以看到,Keyguard第一个涉及到的是KeyguardDisplayManager,其由KeyguardViewMediator这个界面中介调用。

           首先,由SystemServer的startSystemUi方法里的StartServiceAsUser连接到SystemUIService。再由SystemUIService(SystemUI/src/com/android/systemui/SystemUIService.java)里的onCreate函数调用(就这一个有用的函数)startServicesIfNeeded方法,开始SystemUI的初始化

    1     static final void startSystemUi(Context context) {
    2         Intent intent = new Intent();
    3         intent.setComponent(new ComponentName("com.android.systemui",
    4                     "com.android.systemui.SystemUIService"));
    5         intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);
    6         //Slog.d(TAG, "Starting service: " + intent);
    7         context.startServiceAsUser(intent, UserHandle.SYSTEM);
    8     }
    1 public class SystemUIService extends Service {
    2 
    3     @Override
    4     public void onCreate() {
    5         super.onCreate();
    6         ((SystemUIApplication) getApplication()).startServicesIfNeeded();
    7     }

       

           KeyguardViewMediator是SystemUIApplication名为SERVICES数组里的一员,这个数组的东西都是SystemUI要load的Service的class,这个框架流程在start的时候,调用这些类的start方法,并在bootcompleted的时候调用这些类的onbootcompleted方法。

     1  /**
     2 44     * The classes of the stuff to start.
     3 45     */
     4 46    private final Class<?>[] SERVICES = new Class[] {
     5 47            com.android.systemui.tuner.TunerService.class,
     6 48            com.android.systemui.keyguard.KeyguardViewMediator.class,
     7 49            com.android.systemui.recents.Recents.class,
     8 50            com.android.systemui.volume.VolumeUI.class,
     9 51            Divider.class,
    10 52            com.android.systemui.statusbar.SystemBars.class,
    11 53            com.android.systemui.usb.StorageNotification.class,
    12 54            com.android.systemui.power.PowerUI.class,
    13 55            com.android.systemui.media.RingtonePlayer.class,
    14 56            com.android.systemui.keyboard.KeyboardUI.class,
    15 57            com.android.systemui.tv.pip.PipUI.class,
    16 58            com.android.systemui.shortcut.ShortcutKeyDispatcher.class,
    17 59            com.android.systemui.VendorServices.class
    18 60    };

        可以看到,在SystemUIApplication这个类的startServicesIfNeeded里,会依次调用SERVICES里的start函数,这里会先调用com.android.systemui.keyguard.KeyguardViewMediator的start方法

     1 final int N = services.length;
     2 156        for (int i=0; i<N; i++) {
     3 157            Class<?> cl = services[i];
     4 158            if (DEBUG) Log.d(TAG, "loading: " + cl);
     5 159            try {
     6 160                Object newService = SystemUIFactory.getInstance().createInstance(cl);
     7 161                mServices[i] = (SystemUI) ((newService == null) ? cl.newInstance() : newService);
     8 162            } catch (IllegalAccessException ex) {
     9 163                throw new RuntimeException(ex);
    10 164            } catch (InstantiationException ex) {
    11 165                throw new RuntimeException(ex);
    12 166            }
    13 167
    14 168            mServices[i].mContext = this;
    15 169            mServices[i].mComponents = mComponents;
    16 170            if (DEBUG) Log.d(TAG, "running: " + mServices[i]);
    17 171            mServices[i].start();
    18 172
    19 173            if (mBootCompleted) {
    20 174                mServices[i].onBootCompleted();
    21 175            }
    22 176        }
    23 177        mServicesStarted = true;

        

          下面看下KeyguardViewMediator及其start方法

           这个类位于SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java比较长。第三方客户端也可以通过调用KeyguardManager这个类来获取和修改锁屏的信息、状态,这个类是锁屏操作的binder server基础类。

           KeyguardViewMediator是抽象类SystemUI的一个具体实现子类,SystemUI这个类的主要方法是putComponent和getComponent,保存和获取相应类对应的实际组件的映射。还用mContext和mComponents保存相应的SystemUIApplication实例和其中名为component的hashmap。

           KyeguardViewMediator总体负责所有的锁屏状态,并根据状态来决定调用哪些组件。

           KeyguardViewMediator的start方法很简单,初始化锁屏状态,把KeyguardViewMediator的class和KeyguardViewMediator建立映射。

    1  @Override
    2 699    public void start() {
    3 700        synchronized (this) {
    4 701            setupLocked();
    5 702        }
    6 703        putComponent(KeyguardViewMediator.class, this);
    7 704    }

            初始化的过程在setupLocked方法里完成,首先获取系统的PowerManagerService,WindowManagerService,TrustManagerService并初始化一把partial wakelock锁

    1            mPM = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
    2 635        mWM = WindowManagerGlobal.getWindowManagerService();
    3 636        mTrustManager = (TrustManager) mContext.getSystemService(Context.TRUST_SERVICE);
    4 637
    5 638        mShowKeyguardWakeLock = mPM.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "show keyguard");
    6 639        mShowKeyguardWakeLock.setReferenceCounted(false);

              随后注册DELAYED_KEYGUARD_ACTION和DELAYED_LOCK_PROFILE_ACTION这两个Intent的broadcastreceiver

    1 641        mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(DELAYED_KEYGUARD_ACTION));
    2 642        mContext.registerReceiver(
    3 643                mBroadcastReceiver, new IntentFilter(DELAYED_LOCK_PROFILE_ACTION));

               然后创建Keyguard包里的KeyguardDisplayManager和KeyguardUpdateMonitor,还有锁屏模式工具类,获取AlarmManagerService,给KeyguardUpdateMonitor设置当前的用户。

    1 645        mKeyguardDisplayManager = new KeyguardDisplayManager(mContext);
    2 646
    3 647        mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    4 648
    5 649        mUpdateMonitor = KeyguardUpdateMonitor.getInstance(mContext);
    6 650
    7 651        mLockPatternUtils = new LockPatternUtils(mContext);
    8 652        KeyguardUpdateMonitor.setCurrentUser(ActivityManager.getCurrentUser());

                然后设置锁屏状态的变量并调用锁屏状态改变回调函数表,通知TrustManager

    1 654        // Assume keyguard is showing (unless it's disabled) until we know for sure...
    2 655        setShowingLocked(!shouldWaitForProvisioning() && !mLockPatternUtils.isLockScreenDisabled(
    3 656                KeyguardUpdateMonitor.getCurrentUser()));
    4 657        updateInputRestrictedLocked();
    5 658        mTrustManager.reportKeyguardShowingChanged();

                然后设置视图显示,通过SystemUIFactory获取StatusBarKeyguardViewManager,并把视图中介回调(mViewMediatorCallback)和锁屏模式工具(mLockPatternUtils)传入。

    1 660        mStatusBarKeyguardViewManager =
    2 661                SystemUIFactory.getInstance().createStatusBarKeyguardViewManager(mContext,
    3 662                        mViewMediatorCallback, mLockPatternUtils);

                SystemUIFactory里的code如下

    1 84    public StatusBarKeyguardViewManager createStatusBarKeyguardViewManager(Context context,
    2 85            ViewMediatorCallback viewMediatorCallback, LockPatternUtils lockPatternUtils) {
    3 86        return new StatusBarKeyguardViewManager(context, viewMediatorCallback, lockPatternUtils);
    4 87    }

                StatusBarKeyguardViewManager是SystemUI中的一个状态栏组件,是锁屏的视图。

                最后设置锁屏和解锁的声音的文件和音量,获取设备交互状态,加载锁屏隐藏的动画 com.android.internal.R.anim.lock_screen_behind_enter 。

    
    
      663        final ContentResolver cr = mContext.getContentResolver();
      664
      665        mDeviceInteractive = mPM.isInteractive();

    1
    667 mLockSounds = new SoundPool(1, AudioManager.STREAM_SYSTEM, 0); 2 668 String soundPath = Settings.Global.getString(cr, Settings.Global.LOCK_SOUND); 3 669 if (soundPath != null) { 4 670 mLockSoundId = mLockSounds.load(soundPath, 1); 5 671 } 6 672 if (soundPath == null || mLockSoundId == 0) { 7 673 Log.w(TAG, "failed to load lock sound from " + soundPath); 8 674 } 9 675 soundPath = Settings.Global.getString(cr, Settings.Global.UNLOCK_SOUND); 10 676 if (soundPath != null) { 11 677 mUnlockSoundId = mLockSounds.load(soundPath, 1); 12 678 } 13 679 if (soundPath == null || mUnlockSoundId == 0) { 14 680 Log.w(TAG, "failed to load unlock sound from " + soundPath); 15 681 } 16 682 soundPath = Settings.Global.getString(cr, Settings.Global.TRUSTED_SOUND); 17 683 if (soundPath != null) { 18 684 mTrustedSoundId = mLockSounds.load(soundPath, 1); 19 685 } 20 686 if (soundPath == null || mTrustedSoundId == 0) { 21 687 Log.w(TAG, "failed to load trusted sound from " + soundPath); 22 688 } 23 689 24 690 int lockSoundDefaultAttenuation = mContext.getResources().getInteger( 25 691 com.android.internal.R.integer.config_lockSoundVolumeDb); 26 692 mLockSoundVolume = (float)Math.pow(10, (float)lockSoundDefaultAttenuation/20); 27 693 28 694 mHideAnimation = AnimationUtils.loadAnimation(mContext, 29 695 com.android.internal.R.anim.lock_screen_behind_enter); 30 696 }

              然后SystemUIApplication会调用  mServices[i].onBootCompleted 方法,会在KeyguardViemMediator的start方法后调用,来发出Intent ACTION_BOOT_COMPLETED,通知其他组件锁屏初始化完成

              上边是锁屏的初始化过程,然后就是锁屏的加载过程。锁屏界面的加载有两个地方,第一个是第一次开机的时候;第二个是在灭屏后,这个时候会预加载锁屏界面加速亮屏显示。

               第一次开机时,,

               在按住Power键灭屏的时候,流程如下

               可以看到,KeyguardViewMediator里有两个回调函数被涉及

               第一个是onStartedGoingToSleep。这个方法里做锁屏的一些预处理,并发出锁屏通知给KeyguardUpdateMonitor(这里的状态太多了)

     1 723    /**
     2 724     * Called to let us know the screen was turned off.
     3 725     * @param why either {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_USER} or
     4 726     *   {@link android.view.WindowManagerPolicy#OFF_BECAUSE_OF_TIMEOUT}.
     5 727     */
     6 728    public void onStartedGoingToSleep(int why) {
     7 729        if (DEBUG) Log.d(TAG, "onStartedGoingToSleep(" + why + ")");
     8 730        synchronized (this) {
     9 731            mDeviceInteractive = false;
    10 732            mGoingToSleep = true;
    11 733
    12 734            // Lock immediately based on setting if secure (user has a pin/pattern/password).
    13 735            // This also "locks" the device when not secure to provide easy access to the
    14 736            // camera while preventing unwanted input.
    15 737            int currentUser = KeyguardUpdateMonitor.getCurrentUser();
    16 738            final boolean lockImmediately =
    17 739                    mLockPatternUtils.getPowerButtonInstantlyLocks(currentUser)
    18 740                            || !mLockPatternUtils.isSecure(currentUser);
    19 741            long timeout = getLockTimeout(KeyguardUpdateMonitor.getCurrentUser());
    20 742            mLockLater = false;
    21 743            if (mExitSecureCallback != null) {
    22 744                if (DEBUG) Log.d(TAG, "pending exit secure callback cancelled");
    23 745                try {
    24 746                    mExitSecureCallback.onKeyguardExitResult(false);
    25 747                } catch (RemoteException e) {
    26 748                    Slog.w(TAG, "Failed to call onKeyguardExitResult(false)", e);
    27 749                }
    28 750                mExitSecureCallback = null;
    29 751                if (!mExternallyEnabled) {
    30 752                    hideLocked();
    31 753                }
    32 754            } else if (mShowing) {
    33 755                mPendingReset = true;
    34 756            } else if ((why == WindowManagerPolicy.OFF_BECAUSE_OF_TIMEOUT && timeout > 0)
    35 757                    || (why == WindowManagerPolicy.OFF_BECAUSE_OF_USER && !lockImmediately)) {
    36 758                doKeyguardLaterLocked(timeout);
    37 759                mLockLater = true;
    38 760            } else if (!mLockPatternUtils.isLockScreenDisabled(currentUser)) {
    39 761                mPendingLock = true;
    40 762            }
    41 763
    42 764            if (mPendingLock) {
    43 765                playSounds(true);
    44 766            }
    45 767        }
    46 768        KeyguardUpdateMonitor.getInstance(mContext).dispatchStartedGoingToSleep(why);
    47 769        notifyStartedGoingToSleep();
    48 770    }

               第二个是onFinishedGoingToSleep,可以看到核心方法是doKeyguardLocked和doKeyguardForChildProfilesLocked

     1 772    public void onFinishedGoingToSleep(int why, boolean cameraGestureTriggered) {
     2 773        if (DEBUG) Log.d(TAG, "onFinishedGoingToSleep(" + why + ")");
     3 774        synchronized (this) {
     4 775            mDeviceInteractive = false;
     5 776            mGoingToSleep = false;
     6 777
     7 778            resetKeyguardDonePendingLocked();
     8 779            mHideAnimationRun = false;
     9 780
    10 781            notifyFinishedGoingToSleep();
    11 782
    12 783            if (cameraGestureTriggered) {
    13 784                Log.i(TAG, "Camera gesture was triggered, preventing Keyguard locking.");
    14 785
    15 786                // Just to make sure, make sure the device is awake.
    16 787                mContext.getSystemService(PowerManager.class).wakeUp(SystemClock.uptimeMillis(),
    17 788                        "com.android.systemui:CAMERA_GESTURE_PREVENT_LOCK");
    18 789                mPendingLock = false;
    19 790                mPendingReset = false;
    20 791            }
    21 792
    22 793            if (mPendingReset) {
    23 794                resetStateLocked();
    24 795                mPendingReset = false;
    25 796            }
    26 797
    27 798            if (mPendingLock) {
    28 799                doKeyguardLocked(null);
    29 800                mPendingLock = false;
    30 801            }
    31 802
    32 803            // We do not have timeout and power button instant lock setting for profile lock.
    33 804            // So we use the personal setting if there is any. But if there is no device
    34 805            // we need to make sure we lock it immediately when the screen is off.
    35 806            if (!mLockLater && !cameraGestureTriggered) {
    36 807                doKeyguardForChildProfilesLocked();
    37 808            }
    38 809
    39 810        }
    40 811        KeyguardUpdateMonitor.getInstance(mContext).dispatchFinishedGoingToSleep(why);
    41 812    }

             doKeyguardLocked 会先判断要不要锁屏,如果需要,则调用方法showLocked

     1 1192    /**
     2 1193     * Enable the keyguard if the settings are appropriate.
     3 1194     */
     4 1195    private void doKeyguardLocked(Bundle options) {
     5 1196        // if another app is disabling us, don't show
     6 1197        if (!mExternallyEnabled) {
     7 1198            if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled");
     8 1199
     9 1200            // note: we *should* set mNeedToReshowWhenReenabled=true here, but that makes
    10 1201            // for an occasional ugly flicker in this situation:
    11 1202            // 1) receive a call with the screen on (no keyguard) or make a call
    12 1203            // 2) screen times out
    13 1204            // 3) user hits key to turn screen back on
    14 1205            // instead, we reenable the keyguard when we know the screen is off and the call
    15 1206            // ends (see the broadcast receiver below)
    16 1207            // TODO: clean this up when we have better support at the window manager level
    17 1208            // for apps that wish to be on top of the keyguard
    18 1209            return;
    19 1210        }
    20 1211
    21 1212        // if the keyguard is already showing, don't bother
    22 1213        if (mStatusBarKeyguardViewManager.isShowing()) {
    23 1214            if (DEBUG) Log.d(TAG, "doKeyguard: not showing because it is already showing");
    24 1215            resetStateLocked();
    25 1216            return;
    26 1217        }
    27 1218
    28 1219        // In split system user mode, we never unlock system user.
    29 1220        if (!mustNotUnlockCurrentUser()
    30 1221                || !mUpdateMonitor.isDeviceProvisioned()) {
    31 1222
    32 1223            // if the setup wizard hasn't run yet, don't show
    33 1224            final boolean requireSim = !SystemProperties.getBoolean("keyguard.no_require_sim", false);
    34 1225            final boolean absent = SubscriptionManager.isValidSubscriptionId(
    35 1226                    mUpdateMonitor.getNextSubIdForState(IccCardConstants.State.ABSENT));
    36 1227            final boolean disabled = SubscriptionManager.isValidSubscriptionId(
    37 1228                    mUpdateMonitor.getNextSubIdForState(IccCardConstants.State.PERM_DISABLED));
    38 1229            final boolean lockedOrMissing = mUpdateMonitor.isSimPinSecure()
    39 1230                    || ((absent || disabled) && requireSim);
    40 1231
    41 1232            if (!lockedOrMissing && shouldWaitForProvisioning()) {
    42 1233                if (DEBUG) Log.d(TAG, "doKeyguard: not showing because device isn't provisioned"
    43 1234                        + " and the sim is not locked or missing");
    44 1235                return;
    45 1236            }
    46 1237
    47 1238            if (mLockPatternUtils.isLockScreenDisabled(KeyguardUpdateMonitor.getCurrentUser())
    48 1239                    && !lockedOrMissing) {
    49 1240                if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off");
    50 1241                return;
    51 1242            }
    52 1243
    53 1244            if (mLockPatternUtils.checkVoldPassword(KeyguardUpdateMonitor.getCurrentUser())) {
    54 1245                if (DEBUG) Log.d(TAG, "Not showing lock screen since just decrypted");
    55 1246                // Without this, settings is not enabled until the lock screen first appears
    56 1247                setShowingLocked(false);
    57 1248                hideLocked();
    58 1249                mUpdateMonitor.reportSuccessfulStrongAuthUnlockAttempt();
    59 1250                return;
    60 1251            }
    61 1252        }
    62 1253
    63 1254        if (DEBUG) Log.d(TAG, "doKeyguard: showing the lock screen");
    64 1255        showLocked(options);
    65 1256    }

               showLocked方法会发送SHOW消息

     1 1332    /**
     2 1333     * Send message to keyguard telling it to show itself
     3 1334     * @see #handleShow
     4 1335     */
     5 1336    private void showLocked(Bundle options) {
     6 1337        Trace.beginSection("KeyguardViewMediator#showLocked aqcuiring mShowKeyguardWakeLock");
     7 1338        if (DEBUG) Log.d(TAG, "showLocked");
     8 1339        // ensure we stay awake until we are finished displaying the keyguard
     9 1340        mShowKeyguardWakeLock.acquire();
    10 1341        Message msg = mHandler.obtainMessage(SHOW, options);
    11 1342        mHandler.sendMessage(msg);
    12 1343        Trace.endSection();
    13 1344    }

               handleShow方法就会被调用,显示mStatusBarKeyguardViewManagermKeyguardDisplayManager的show方法。

     1 1625    /**
     2 1626     * Handle message sent by {@link #showLocked}.
     3 1627     * @see #SHOW
     4 1628     */
     5 1629    private void handleShow(Bundle options) {
     6 1630        Trace.beginSection("KeyguardViewMediator#handleShow");
     7 1631        final int currentUser = KeyguardUpdateMonitor.getCurrentUser();
     8 1632        if (mLockPatternUtils.isSecure(currentUser)) {
     9 1633            mLockPatternUtils.getDevicePolicyManager().reportKeyguardSecured(currentUser);
    10 1634        }
    11 1635        synchronized (KeyguardViewMediator.this) {
    12 1636            if (!mSystemReady) {
    13 1637                if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready.");
    14 1638                return;
    15 1639            } else {
    16 1640                if (DEBUG) Log.d(TAG, "handleShow");
    17 1641            }
    18 1642
    19 1643            setShowingLocked(true);
    20 1644            mStatusBarKeyguardViewManager.show(options);
    21 1645            mHiding = false;
    22 1646            mWakeAndUnlocking = false;
    23 1647            resetKeyguardDonePendingLocked();
    24 1648            mHideAnimationRun = false;
    25 1649            updateActivityLockScreenState();
    26 1650            adjustStatusBarLocked();
    27 1651            userActivity();
    28 1652
    29 1653            mShowKeyguardWakeLock.release();
    30 1654        }
    31 1655        mKeyguardDisplayManager.show();
    32 1656        Trace.endSection();
    33 1657    }

               

               还有一种情况是超时灭屏,与上边的按住Power键灭屏流程基本一样

    基本流程分析完了,下面看看Keyguard里的具体每个类

    先看KeyguardDisplayManager这个类,这个类是控制手机远程显示的。如果手机远程连接上了电视这样的设备,就先一个一个KeyguardPresentation对话框,是个时钟

     1     protected void updateDisplays(boolean showing) {
     2         if (showing) {
     3             MediaRouter.RouteInfo route = mMediaRouter.getSelectedRoute(
     4                     MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY);
     5             boolean useDisplay = route != null
     6                     && route.getPlaybackType() == MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
     7             Display presentationDisplay = useDisplay ? route.getPresentationDisplay() : null;
     8 
     9             if (mPresentation != null && mPresentation.getDisplay() != presentationDisplay) {
    10                 if (DEBUG) Slog.v(TAG, "Display gone: " + mPresentation.getDisplay());
    11                 mPresentation.dismiss();
    12                 mPresentation = null;
    13             }
    14 
    15             if (mPresentation == null && presentationDisplay != null) {
    16                 if (DEBUG) Slog.i(TAG, "Keyguard enabled on display: " + presentationDisplay);
    17                 mPresentation = new KeyguardPresentation(mContext, presentationDisplay,
    18                         R.style.keyguard_presentation_theme);
    19                 mPresentation.setOnDismissListener(mOnDismissListener);
    20                 try {
    21                     mPresentation.show();
    22                 } catch (WindowManager.InvalidDisplayException ex) {
    23                     Slog.w(TAG, "Invalid display:", ex);
    24                     mPresentation = null;
    25                 }
    26             }
    27         } else {
    28             if (mPresentation != null) {
    29                 mPresentation.dismiss();
    30                 mPresentation = null;
    31             }
    32         }
    33     }
  • 相关阅读:
    python处理中文字符的一点经验
    15个最受欢迎的Python开源框架
    一道有趣的面试题——扔鸡蛋问题
    归并排序算法学习笔记
    快速排序算法学习笔记
    python遇到‘u’开头的unicode编码
    工程实践中最常用的数据结构与算法
    OCR与车牌识别相关
    基于暗通道评估可见度流程
    Git操作
  • 原文地址:https://www.cnblogs.com/cascle/p/7053688.html
Copyright © 2020-2023  润新知