APK安裝(一)—— PMS原理分析

一、前言

APK安裝概述 中曾提及apk有四種安裝場景,但無論是哪一種方式,最終會提交給 PackageManagerService 處理,只是前置的處理鏈路 不同,所以本篇先對 PMS 這一主要過程進行分析。frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java 【基于Android 9.0】

二、PMS 做了哪些工作

1、對特定的一些系統(tǒng)進程信息進行設置處理,并保存到 Settings
2、解析 /etc/permissions 下相關xml文件取得系統(tǒng)相關權限、系統(tǒng)具備的相關功能等信息
3、解析 /data/system/package.xml 文件獲取已安裝應用的相關信息
4、對相關的 apk 和 jar 進行 dex 優(yōu)化處理,主要是 /system/framework 目錄下的相關jar和apk
5、依據(jù) sharedUserId 這個配置來確定 apk 運行在哪個進程,然后把運行的相關進程信息加入到 Settings 中,使得系統(tǒng)可以知道每個 apk 運行在哪個進程中
6、解析 AndroidManifest.xml 文件,提煉文件中的節(jié)點信息
7、掃描本地文件,主要針對系統(tǒng)應用、本地安裝應用等等
8、管理本地 apk ,包括安裝、刪除等

三、安裝過程

3.1 準備工作

前面說到 APK 的信息會提交給 PMS 進行安裝的一系列工作,具體是通過 PackageHandler 發(fā)送消息來驅(qū)動 APK 的復制和安裝,其時序圖如下:

上相過程中有幾點需要說明:
1、在 installStage 方法中創(chuàng)建了 InstallParams 對象,它對應于包的安裝數(shù)據(jù),并創(chuàng)建 INIT_COPY 消息并發(fā)送給 PackageHandler 進行處理;
2、PackageHandler 在處理 INIT_COPY 消息時,會先判斷是否綁定了 DefaultContainerService ,這是用于檢查和賦值可移動文件的服務,比較耗時,所以和 PMS 并沒有運行在同一個進程中,它們之間通過 IMediaContainerService 進行 IPC 通信,沒有綁定則會進行綁定,之后 \color{red}{\small{將安裝的請求添加到 mPendingInstalls 集合中,等待處理}}

DefaultContainerConnection 同樣是定義在 PMS 中,執(zhí)行鏈路如下:

PackageHandler -> doHandleMessage(INIT_COPY)
               -> connectToService()
                  {
                     連接成功
                     mBound = true;
                  }
               -> bindServiceAsUser()
               -> DefaultContainerConnection 
                    -> onServiceConnected()
                    -> sendMessage(MCS_BOUND)

3、發(fā)送 MCS_BOUND 消息時,根據(jù)發(fā)送的 Message 是否帶 Object 分為兩種,如下所示:

       void doHandleMessage(Message msg) {
           switch (msg.what) {
               case INIT_COPY: {
                   ......
                   //mBound用于標識是否綁定了服務,默認值為false
                   if (!mBound) {
                        ......
                       if (!connectToService()) {
                        // 綁定 DefaultContainerConnection 成功之后會發(fā)送 
                        // mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, Object));
                        ......
                         //綁定服務失敗則return
                           return;
                       } else {
                           //綁定服務成功,將請求添加到ArrayList類型的mPendingInstalls中,等待處理
                           mPendingInstalls.add(idx, params);
                       }
                   } else {
                     //已經(jīng)綁定服務
                      mHandler.sendEmptyMessage(MCS_BOUND);
                   }
                   break;
               }
               ......
               }
   }
}

4、 MCS_BOUND 消息的處理:

case MCS_BOUND: {
            if (DEBUG_INSTALL) Slog.i(TAG, "mcs_bound");
            if (msg.obj != null) {
            ...
            }
            if (mContainerService == null) {//1
             ...
            } else if (mPendingInstalls.size() > 0) {//2
                          HandlerParams params = mPendingInstalls.get(0);//3
                        if (params != null) {
                            Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, "queueInstall",
                                    System.identityHashCode(params));
                            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "startCopy");
                            if (params.startCopy()) {//4
                                if (DEBUG_SD_INSTALL) Log.i(TAG,
                                        "Checking for more work or unbind...");
                                 //如果APK安裝成功,刪除本次安裝請求
                                if (mPendingInstalls.size() > 0) {
                                    mPendingInstalls.remove(0);
                                }
                                if (mPendingInstalls.size() == 0) {
                                    if (mBound) {
                                    //如果沒有安裝請求了,發(fā)送解綁服務的請求
                                        if (DEBUG_SD_INSTALL) Log.i(TAG,
                                                "Posting delayed MCS_UNBIND");
                                        removeMessages(MCS_UNBIND);
                                        Message ubmsg = obtainMessage(MCS_UNBIND);
                                        sendMessageDelayed(ubmsg, 10000);
                                    }
                                } else {
                                    if (DEBUG_SD_INSTALL) Log.i(TAG,
                                            "Posting MCS_BOUND for next work");
                                   //如果還有其他的安裝請求,接著發(fā)送MCS_BOUND消息繼續(xù)處理剩余的安裝請求      
                                    mHandler.sendEmptyMessage(MCS_BOUND);
                                }
                            }
                            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
                        }else {
                        Slog.w(TAG, "Empty queue");
                    }
            break;
        }

3.2 復制APK

3.2.1 復制過程時序圖

HandlerParamsPMS 中的抽象類,它的實現(xiàn)類為 PMS 的內(nèi)部類 InstallParams。HandlerParamsstartCopy 方法如下所示:

3.2.2 復制過程的源碼分析

PackageManagerService.java#HandlerParams

final boolean startCopy() {
           boolean res;
           try {
               if (DEBUG_INSTALL) Slog.i(TAG, "startCopy " + mUser + ": " + this);
               //startCopy方法嘗試的次數(shù),超過了4次,就放棄這個安裝請求
               if (++mRetries > MAX_RETRIES) {
                   Slog.w(TAG, "Failed to invoke remote methods on default container service. Giving up");
              //MCS_GIVE_UP類型消息,將本次安裝請求從安裝請求隊列mPendingInstalls中移除掉
                   mHandler.sendEmptyMessage(MCS_GIVE_UP);
                   handleServiceError();
                   return false;
               } else {
                   handleStartCopy(); // 注釋①
                   res = true;
               }
           } catch (RemoteException e) {
               if (DEBUG_INSTALL) Slog.i(TAG, "Posting install MCS_RECONNECT");
               mHandler.sendEmptyMessage(MCS_RECONNECT);
               res = false;
           }
           handleReturnCode(); 
           return res;
       }

注釋① 處調(diào)用抽象方法 handleStartCopy ,具體實現(xiàn)在 InstallParams 中,如下所示:
PackageManagerService.java#InstallParams

public void handleStartCopy() throws RemoteException {
       ...
       //確定APK的安裝位置。onSd:安裝到SD卡, onInt:內(nèi)部存儲即Data分區(qū),ephemeral:安裝到臨時存儲(Instant Apps安裝)            
       final boolean onSd = (installFlags & PackageManager.INSTALL_EXTERNAL) != 0;
       final boolean onInt = (installFlags & PackageManager.INSTALL_INTERNAL) != 0;
       final boolean ephemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
       PackageInfoLite pkgLite = null;
       if (onInt && onSd) {
         // APK不能同時安裝在SD卡和Data分區(qū)
           Slog.w(TAG, "Conflicting flags specified for installing on both internal and external");
           ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
         //安裝標志沖突,Instant Apps不能安裝到SD卡中
       } else if (onSd && ephemeral) {
           Slog.w(TAG,  "Conflicting flags specified for installing ephemeral on external");
           ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
       } else {
            //獲取APK的少量的信息
           pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,
                   packageAbiOverride);
           if (DEBUG_EPHEMERAL && ephemeral) {
               Slog.v(TAG, "pkgLite for install: " + pkgLite);
           }
       ...
       if (ret == PackageManager.INSTALL_SUCCEEDED) {
            //判斷安裝的位置
           int loc = pkgLite.recommendedInstallLocation;
           if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION) {
               ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
           } else if (loc == PackageHelper.RECOMMEND_FAILED_ALREADY_EXISTS) {
               ret = PackageManager.INSTALL_FAILED_ALREADY_EXISTS;
           } 
           ...
           }else{
             loc = installLocationPolicy(pkgLite);   // 注釋①
             ...
           }
       }
       //根據(jù)InstallParams創(chuàng)建InstallArgs對象
       final InstallArgs args = createInstallArgs(this);       // 注釋②
       mArgs = args;
       if (ret == PackageManager.INSTALL_SUCCEEDED) {
              ...
           if (!origin.existing && requiredUid != -1
                   && isVerificationEnabled(
                         verifierUser.getIdentifier(), installFlags, installerUid)) {
                 ...
           } else{
               ret = args.copyApk(mContainerService, true);     // 注釋③
           }
       }
       mRet = ret;
   }

1、注釋① 處確定了 APK 的安裝位置。
2、注釋②處創(chuàng)建 InstallArgs 對象,此對象是一個抽象類,定義了 APK 的復制和重命名APK等安裝邏輯,在 Android 8.x 及之前的版本中有三個子類:FileInstallArgs、AsecInstallArgs、MoveInstallArgs。其中 FileInstallArgs 用于處理安裝到非ASEC的存儲空間的APK,即內(nèi)部存儲空間(Data分區(qū));AsecInstallArgs 用于處理安裝到ASEC(mnt/asec)即SD卡中的APK;MoveInstallArgs 用于處理已安裝APK的移動的邏輯;但在 Android 9.x 之后已經(jīng)去掉了 AsecInstallArgs ,
3、注釋③ 處調(diào)用 InstallArgscopyApk 方法,這里以 FileInstallArgs 的實現(xiàn)為例,內(nèi)部會調(diào)用 FileInstallArgsdoCopyApk 方法:

private int doCopyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {
        ...
         try {
             final boolean isEphemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
             //創(chuàng)建臨時文件存儲目錄
             final File tempDir =
                     mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral);  // 注釋①
             codeFile = tempDir;
             resourceFile = tempDir;
         } catch (IOException e) {
             Slog.w(TAG, "Failed to create copy file: " + e);
             return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
         }
         ...
         int ret = PackageManager.INSTALL_SUCCEEDED;
         ret = imcs.copyPackage(origin.file.getAbsolutePath(), target);  // 注釋②
         ...
         return ret;
     }

1、注釋① 處用于創(chuàng)建臨時存儲目錄,比如 /data/app/vmdl18300388.tmp,其中 18300388 是安裝的 sessionId
2、注釋②處通過 IMediaContainerService 跨進程調(diào)用 DefaultContainerServicecopyPackage 方法,這個方法會在 DefaultContainerService 所在的進程中將 APK 復制到臨時存儲目錄,比如 /data/app/vmdl18300388.tmp/base.apk ,至此 APK 的復制工作結束。

3.3 APK的安裝

3.3.1 APK安裝時序圖

在上述 APK 的賦值調(diào)用鏈的過程中,在 HandlerParamsstartCopy 方法中,會調(diào)用 handleReturnCode 方法,時序圖如下:

3.3.2 安裝源碼分析

PackageManagerService#handleReturnCode:

 void handleReturnCode() {
    if (mArgs != null) {
        processPendingInstall(mArgs, mRet);
    }
}

    private void processPendingInstall(final InstallArgs args, final int currentStatus) {
        mHandler.post(new Runnable() {
            public void run() {
                mHandler.removeCallbacks(this);
                PackageInstalledInfo res = new PackageInstalledInfo();
                res.setReturnCode(currentStatus);
                res.uid = -1;
                res.pkg = null;
                res.removedInfo = null;
                if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
                    //安裝前處理
                    args.doPreInstall(res.returnCode); //注釋①
                    synchronized (mInstallLock) {
                        installPackageTracedLI(args, res); //注釋②
                    }
                    //安裝后收尾
                    args.doPostInstall(res.returnCode, res.uid); //注釋③
                }
              ...
                  // 安裝結束發(fā)送消息
                  Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);  // 注釋⑤
                  mHandler.sendMessage(msg);
            }
        });
    }

注釋① 處檢查APK的狀態(tài),在安裝前確保安裝環(huán)境的可靠,如果不可靠會清除復制的APK文件,注釋③ 處會檢測是否安裝成功,失敗則刪除安裝相關的目錄和文件。安裝完成之后在 注釋⑤ 處會發(fā)送 POST_INSALL 消息通知已安裝完成,此處稍后會說明。
注釋② 處的 installPackageTracedLI 會調(diào)用 PMSinstallPackageLI 方法:
PackageManagerService.java#installPackageLI:

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
    ...
    PackageParser pp = new PackageParser();
    pp.setSeparateProcesses(mSeparateProcesses);
    pp.setDisplayMetrics(mMetrics);
    pp.setCallback(mPackageParserCallback);
    Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parsePackage");
    final PackageParser.Package pkg;
    try {
        //解析APK
        pkg = pp.parsePackage(tmpPackageFile, parseFlags);  //注釋①
    } catch (PackageParserException e) {
        res.setError("Failed parse during installPackageLI", e);
        return;
    } finally {
        Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
    }
    ...
    pp = null;
    String oldCodePath = null;
    boolean systemApp = false;
    synchronized (mPackages) {
        // 檢查APK是否存在
        if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {
            String oldName = mSettings.getRenamedPackageLPr(pkgName);//獲取沒被改名前的包名
            if (pkg.mOriginalPackages != null
                    && pkg.mOriginalPackages.contains(oldName)
                    && mPackages.containsKey(oldName)) {
                pkg.setPackageName(oldName);  //注釋②
                pkgName = pkg.packageName;
                replace = true;//設置標志位表示是替換安裝
                if (DEBUG_INSTALL) Slog.d(TAG, "Replacing existing renamed package: oldName="
                        + oldName + " pkgName=" + pkgName);
            } 
            ...
        }
        PackageSetting ps = mSettings.mPackages.get(pkgName);
        //查看Settings中是否存有要安裝的APK的信息,如果有就獲取簽名信息
        if (ps != null) {   //注釋③
            if (DEBUG_INSTALL) Slog.d(TAG, "Existing package: " + ps);
            PackageSetting signatureCheckPs = ps;
            if (pkg.applicationInfo.isStaticSharedLibrary()) {
                SharedLibraryEntry libraryEntry = getLatestSharedLibraVersionLPr(pkg);
                if (libraryEntry != null) {
                    signatureCheckPs = mSettings.getPackageLPr(libraryEntry.apk);
                }
            }
            //檢查簽名的正確性
           if (ksms.shouldCheckUpgradeKeySetLocked(signatureCheckPs, scanFlags)) {
                    if (!ksms.checkUpgradeKeySetLocked(signatureCheckPs, pkg)) {
                        res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "
                                + pkg.packageName + " upgrade keys do not match the "
                                + "previously installed version");
                        return;
                    }
            } 
            ...
        }

        int N = pkg.permissions.size();
        for (int i = N-1; i >= 0; i--) {
           //遍歷每個權限,對權限進行處理
            PackageParser.Permission perm = pkg.permissions.get(i);
            BasePermission bp = mSettings.mPermissions.get(perm.info.name);
         
            }
        }
    }
    if (systemApp) {
        if (onExternal) {
            //系統(tǒng)APP不能在SD卡上替換安裝
            res.setError(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
                    "Cannot install updates to system apps on sdcard");
            return;
        } else if (instantApp) {
            //系統(tǒng)APP不能被Instant App替換
            res.setError(INSTALL_FAILED_INSTANT_APP_INVALID,
                    "Cannot update a system app with an instant app");
            return;
        }
    }
    ...
    //重命名臨時文件
    if (!args.doRename(res.returnCode, pkg, oldCodePath)) {  //注釋④
        res.setError(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed rename");
        return;
    }

    startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);

    try (PackageFreezer freezer = freezePackageForInstall(pkgName, installFlags,
            "installPackageLI")) {
       
        if (replace) {  //注釋⑤
         //替換安裝   
           ...
            replacePackageLIF(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,
                    installerPackageName, res, args.installReason);
        } else {
        //安裝新的APK
            installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,
                    args.user, installerPackageName, volumeUuid, res, args.installReason);
        }
    }

    synchronized (mPackages) {
        final PackageSetting ps = mSettings.mPackages.get(pkgName);
        if (ps != null) {
            //更新應用程序所屬的用戶
            res.newUsers = ps.queryInstalledUsers(sUserManager.getUserIds(), true);
            ps.setUpdateAvailable(false /*updateAvailable*/);
        }
        ...
    }
}

這里需要說明幾點:
1、注釋③處,會先檢測 Settings 中保存有要安裝的 APK 信息,則說明安裝該 APK ,因此需要檢驗APK 的簽名信息,確保安全的進行替換。
2、注釋④處,會對臨時文件重新命名,例如 /data/app/vmdl18300388.tmp/base.apk,重命名為 /data/app/包名-oONlnRRPYyleU63AveqbYA==/base.apk。新的包名后面帶上的一串字母和數(shù)字的混合字符串,是使用MD5的方式對隨機生成的16個字符進行加密之后的產(chǎn)物。
3、注釋⑤處,根據(jù) replace 來做區(qū)分,如果是替換安裝就會調(diào)用replacePackageLIF方法,其方法內(nèi)部還會對系統(tǒng)APP和非系統(tǒng)APP進行區(qū)分處理,如果是新安裝APK會調(diào)用installNewPackageLIF方法

PackageManagerService.java#installNewPackageLIF

private void installNewPackageLIF(PackageParser.Package pkg, final int policyFlags,
           int scanFlags, UserHandle user, String installerPackageName, String volumeUuid,
           PackageInstalledInfo res, int installReason) {
       ...
       try {
           //掃描APK
           PackageParser.Package newPackage = scanPackageTracedLI(pkg, policyFlags, scanFlags,
                   System.currentTimeMillis(), user);
           //更新Settings信息
           updateSettingsLI(newPackage, installerPackageName, null, res, user, installReason);
           if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
               //安裝成功后,為新安裝的應用程序準備數(shù)據(jù)
               prepareAppDataAfterInstallLIF(newPackage);

           } else {
               //安裝失敗則刪除APK
               deletePackageLIF(pkgName, UserHandle.ALL, false, null,
                       PackageManager.DELETE_KEEP_DATA, res.removedInfo, true, null);
           }
       } catch (PackageManagerException e) {
           res.setError("Package couldn't be installed in " + pkg.codePath, e);
       }
       Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
   }

3.4 發(fā)送消息

在上面 processPendingInstall 方法的源碼分析中,在 注釋⑤ 處會發(fā)送 POST_INSTALL 消息通知安裝完成,那么接下來就來具體看一看在 PackageHandler 中是怎么處理這個消息的。

class PackageHandler extends Handler {
    void doHandleMessage(Message msg) {
        switch (msg.what) {
            ... ...

            case POST_INSTALL: {
                ... ...
                 handlePackagePostInstall();
                ... ...
                break;
            }
        }
    }
}

private void handlePackagePostInstall(...){
      //   如果已經(jīng)成功的安裝了應用,在發(fā)送廣播之前先授予一些必要的權限
      grantRequestedRuntimePermissions(res.pkg, args.user.getIdentifier(),
                                        args.installGrantPermissions);

      // 安裝完成之后發(fā)送"ACTION_PACKAGE_ADDED"廣播
       sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
                                    packageName, extras, null, null, firstUsers);

      // 如果是升級更新安裝,還會發(fā)送ACTION_PACKAGE_REPLACED和ACTION_MY_PACKAGE_REPLACED廣播
    // 這兩個廣播不同之處在于PACKAGE_REPLACE將攜帶一個extra信息
      sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
                                        packageName, extras, null, null, updateUsers);
      sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED,
                                        null, null, packageName, null, updateUsers);

      // 執(zhí)行gc操作
      Runtime.getRuntime().gc();

      // 調(diào)用FileInstallArgs的doPostDeleteLI進行資源清理
     res.removedInfo.args.doPostDeleteLI(true);

     // 回調(diào)onPackageInstalled方法
     installObserver.onPackageInstalled(res.name, res.returnCode,
                        res.returnMsg, extras);
}

以上為主要的方法摘要,具體可總結為:
1、第一步:這里主要是先將安裝信息從安裝列列表中移除,這個也是前面在processPendingInstall中添加的
2、第二步:安裝成功后,獲取運行時權限
3、第三步:獲取權限后,發(fā)送ACTION_PACKAGE_ADDED廣播,告訴Laucher之流,更新icon
4、第四步:如果是升級更新則在發(fā)送兩條廣播

  • ACTION_PACKAGE_REPLACED:一個新版本的應用安裝到設備上,替換換之前已經(jīng)存在的版本
  • ACTION_MY_PACKAGE_REPLACED:應用的新版本替換舊版本被安裝,只發(fā)給被更新的應用自己

5、第五步:如果安裝包中設置了PRIVATE_FLAG_FORWARD_LOCK或者被要求安裝在SD卡上,則調(diào)用sendResourcesChangedBroadcast方法來發(fā)送一個資源更改的廣播
6、第六步:如果該應用是一個瀏覽器,則要清除瀏覽器設置,重新檢查瀏覽器設置
7、第七步:強制調(diào)用gc,出發(fā)JVM進行垃圾回收操作
8、第八步:刪除舊的安裝信息
9、回調(diào)回調(diào) IPackageInstallObserver2 的 packageInstalled 方法。告訴 PackageInstaller 安裝結果。從而實現(xiàn)了安裝回調(diào)到UI層

3.5 安裝總結

上述幾部分大致說明 PMS 處理 APK 的主要步驟,可總結如下:
1、當 PackageInstallerAPK 的信息提交給 PMS 處理,PMS 會通過向 PackageHandler 發(fā)送消息來驅(qū)動 APK 的復制和安裝工作
2、PMS 發(fā)送 INIT_COPYMCS_BOUND 類型的消息,控制 PackageHandler 來綁定 DefaultContainerService 來完成 APK 的復制等工作
3、復制 APK 完成之后,則開始進行安裝 APK 的流程,包括安裝前的檢查、安裝 APK 和安裝后的收尾工作。

參考

[ 1 ] https://maoao530.github.io/2017/01/18/package-install/
[ 2 ] https://blog.csdn.net/yiranfeng/article/details/103941371
[ 3 ] http://liuwangshu.cn/framework/pms/3-pms-install.html
[ 4 ] https://www.freesion.com/article/5119749905/
[ 5 ] http://www.itdecent.cn/p/9ddb930153b7

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容