前言

采用系统应用安装管理器方式,需要系统签名才可使用,可用于应用商店,负一屏,launcher等应用

静默安装与卸载(基于Android12版本实现)

安装流程

这是我们平时使用的发起显式安装请求,会有弹窗提示安装确认

    public static void install(Context context, File file) {
        Intent installApkIntent = new Intent();
        installApkIntent.setAction(Intent.ACTION_VIEW);
        installApkIntent.addCategory(Intent.CATEGORY_DEFAULT);
        installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //适配8.0需要有权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            boolean hasInstallPermission = context.getPackageManager().canRequestPackageInstalls();
            if (hasInstallPermission) {
                //安装应用
                installApkIntent.setDataAndType(FileProvider.getUriForFile(context.getApplicationContext(), context.getPackageName() + ".file_provider", file), "application/vnd.android.package-archive");
                installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                if (context.getPackageManager().queryIntentActivities(installApkIntent, 0).size() > 0) {
                    context.startActivity(installApkIntent);
                }
            }
        }
    }

系统收到用户安装请求会调用系统内部预置的packageinstaller.apk,首先是启动InstallStart(Activity),在onCreate中构建引用的基本信息,然后传递给PackageInstallerActivity(AlertActivity),代码如下:

        Intent nextActivity = new Intent(intent);
        nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
                | Intent.FLAG_GRANT_READ_URI_PERMISSION);

        // The the installation source as the nextActivity thinks this activity is the source, hence
        // set the originating UID and sourceInfo explicitly
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_ATTRIBUTION_TAG,
                callingAttributionTag);
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
        nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);

        if (isSessionInstall) {
            nextActivity.setClass(this, PackageInstallerActivity.class);
        } else {
            Uri packageUri = intent.getData();

            if (packageUri != null && packageUri.getScheme().equals(
                    ContentResolver.SCHEME_CONTENT)) {
                // [IMPORTANT] This path is deprecated, but should still work. Only necessary
                // features should be added.

                // Copy file to prevent it from being changed underneath this process
                nextActivity.setClass(this, InstallStaging.class);
            } else if (packageUri != null && packageUri.getScheme().equals(
                    PackageInstallerActivity.SCHEME_PACKAGE)) {
                nextActivity.setClass(this, PackageInstallerActivity.class);
            } else {
                Intent result = new Intent();
                result.putExtra(Intent.EXTRA_INSTALL_RESULT,
                        PackageManager.INSTALL_FAILED_INVALID_URI);
                setResult(RESULT_FIRST_USER, result);

                nextActivity = null;
            }
        }

        if (nextActivity != null) {
            startActivity(nextActivity);
        }

PackageInstallerActivity启动后在onCreate中进行一系列解析apk信息的操作,然后在onResume中创建弹窗,再将解析的信息显示在窗口上面,当用户点击确认安装时,再启动InstallInstalling(AlertActivity)

        final Intent intent = getIntent();

        mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
        mCallingAttributionTag = intent.getStringExtra(EXTRA_CALLING_ATTRIBUTION_TAG);
        mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
        mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
                PackageInstaller.SessionParams.UID_UNKNOWN);
        mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
                ? getPackageNameForUid(mOriginatingUid) : null;

        final Uri packageUri;

        if (PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction())) {
            final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
            final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);
            if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {
                Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");
                finish();
                return;
            }

            mSessionId = sessionId;
            packageUri = Uri.fromFile(new File(info.resolvedBaseCodePath));
            mOriginatingURI = null;
            mReferrerURI = null;
        } else {
            mSessionId = -1;
            packageUri = intent.getData();
            mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
            mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
        }

        // if there's nothing to do, quietly slip into the ether
        if (packageUri == null) {
            Log.w(TAG, "Unspecified source");
            setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
            finish();
            return;
        }

        if (DeviceUtils.isWear(this)) {
            showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
            return;
        }

        boolean wasSetUp = processPackageUri(packageUri);
        if (mLocalLOGV) Log.i(TAG, "wasSetUp: " + wasSetUp);

        if (!wasSetUp) {
            return;
        }
        
	··· ···
    @Override
    protected void onResume() {
        super.onResume();

        if (mLocalLOGV) Log.i(TAG, "onResume(): mAppSnippet=" + mAppSnippet);

        if (mAppSnippet != null) {
            // load dummy layout with OK button disabled until we override this layout in
            // startInstallConfirm
            bindUi();
            checkIfAllowedAndInitiateInstall();
        }

        if (mOk != null) {
            mOk.setEnabled(mEnableOk);
        }
    }
	··· ···
    private void bindUi() {
        mAlert.setIcon(mAppSnippet.icon);
        mAlert.setTitle(mAppSnippet.label);
        mAlert.setView(R.layout.install_content_view);
        mAlert.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.install),
                (ignored, ignored2) -> {
                    if (mOk.isEnabled()) {
                        if (mSessionId != -1) {
                            mInstaller.setPermissionsResult(mSessionId, true);
                            finish();
                        } else {
                            startInstall();
                        }
                    }
                }, null);
        mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
                (ignored, ignored2) -> {
                    // Cancel and finish
                    setResult(RESULT_CANCELED);
                    if (mSessionId != -1) {
                        mInstaller.setPermissionsResult(mSessionId, false);
                    }
                    finish();
                }, null);
        setupAlert();

        mOk = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
        mOk.setEnabled(false);

        if (!mOk.isInTouchMode()) {
            mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).requestFocus();
        }
    }
	··· ···
    private void startInstall() {
        // Start subactivity to actually install the application
        Intent newIntent = new Intent();
        newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
                mPkgInfo.applicationInfo);
        newIntent.setData(mPackageURI);
        newIntent.setClass(this, InstallInstalling.class);
        String installerPackageName = getIntent().getStringExtra(
                Intent.EXTRA_INSTALLER_PACKAGE_NAME);
        if (mOriginatingURI != null) {
            newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
        }
        if (mReferrerURI != null) {
            newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
        }
        if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
            newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
        }
        if (installerPackageName != null) {
            newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
                    installerPackageName);
        }
        if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
            newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
        }
        newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
        if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
        startActivity(newIntent);
        finish();
    }
    ··· ···

InstallInstalling启动后在onCreate方法中会先将安装apk时所需要的一些参数提前初始化完毕,然后在onResume中调用安装异步任务,在

    @Override
    protected void onResume() {
        super.onResume();

        // This is the first onResume in a single life of the activity
        if (mInstallingTask == null) {
            PackageInstaller installer = getPackageManager().getPackageInstaller();
            PackageInstaller.SessionInfo sessionInfo = installer.getSessionInfo(mSessionId);

            if (sessionInfo != null && !sessionInfo.isActive()) {
                mInstallingTask = new InstallingAsyncTask();
                mInstallingTask.execute();
            } else {
                // we will receive a broadcast when the install is finished
                mCancelButton.setEnabled(false);
                setFinishOnTouchOutside(false);
            }
        }
    }
 	/**
     * Send the package to the package installer and then register a event result observer that
     * will call {@link #launchFinishBasedOnResult(int, int, String)}
     */
    private final class InstallingAsyncTask extends AsyncTask<Void, Void,
            PackageInstaller.Session> {
        volatile boolean isDone;

        @Override
        protected PackageInstaller.Session doInBackground(Void... params) {
            PackageInstaller.Session session;
            try {
                session = getPackageManager().getPackageInstaller().openSession(mSessionId);
            } catch (IOException e) {
                synchronized (this) {
                    isDone = true;
                    notifyAll();
                }
                return null;
            }

            session.setStagingProgress(0);

            try {
                File file = new File(mPackageURI.getPath());

                try (InputStream in = new FileInputStream(file)) {
                    long sizeBytes = file.length();
                    try (OutputStream out = session
                            .openWrite("PackageInstaller", 0, sizeBytes)) {
                        byte[] buffer = new byte[1024 * 1024];
                        while (true) {
                            int numRead = in.read(buffer);

                            if (numRead == -1) {
                                session.fsync(out);
                                break;
                            }

                            if (isCancelled()) {
                                session.close();
                                break;
                            }

                            out.write(buffer, 0, numRead);
                            if (sizeBytes > 0) {
                                float fraction = ((float) numRead / (float) sizeBytes);
                                session.addProgress(fraction);
                            }
                        }
                    }
                }

                return session;
            } catch (IOException | SecurityException e) {
                Log.e(LOG_TAG, "Could not write package", e);

                session.close();

                return null;
            } finally {
                synchronized (this) {
                    isDone = true;
                    notifyAll();
                }
            }
        }

        @Override
        protected void onPostExecute(PackageInstaller.Session session) {
            if (session != null) {
                Intent broadcastIntent = new Intent(BROADCAST_ACTION);
                broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
                broadcastIntent.setPackage(getPackageName());
                broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId);

                PendingIntent pendingIntent = PendingIntent.getBroadcast(
                        InstallInstalling.this,
                        mInstallId,
                        broadcastIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);

                session.commit(pendingIntent.getIntentSender());
                mCancelButton.setEnabled(false);
                setFinishOnTouchOutside(false);
            } else {
                getPackageManager().getPackageInstaller().abandonSession(mSessionId);

                if (!isCancelled()) {
                    launchFailure(PackageInstaller.STATUS_FAILURE,
                            PackageManager.INSTALL_FAILED_INVALID_APK, null);
                }
            }
        }
    }

到这里安装就可以告一段落了,后续的操作由PackageInstaller.Session内部的IPackageInstallerSession完成,我们需要的功能到这里也够了。

卸载流程

接下来我们来看下卸载的流程,卸载相对于安装来说简单了许多,用户发起一个卸载请求

    public static void uninstall(Context context, String packageName) {
        //获取删除包名的URI
        Uri uri = Uri.parse("package:" + packageName);
        Intent intent = new Intent();
        //设置我们要执行的卸载动作
        intent.setAction(Intent.ACTION_DELETE);
        //设置获取到的URI
        intent.setData(uri);
        context.startActivity(intent);
    }

系统接收到请求后启动UninstallerActivity(Activity),在onCreate里面通过传入的包名加载apk信息,然后创建用户确认弹窗,当用户点击确认按钮时再来到startUninstallProgress(boolean keepData)

    public void startUninstallProgress(boolean keepData) {
    ... ...
        int uninstallId;
        try {
            uninstallId = UninstallEventReceiver.getNewId(this);
        } catch (EventResultPersister.OutOfIdsException e) {
            showGenericError();
            return;
        }

        Intent broadcastIntent = new Intent(this, UninstallFinish.class);

        broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        broadcastIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, mDialogInfo.allUsers);
        broadcastIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, mDialogInfo.appInfo);
        broadcastIntent.putExtra(UninstallFinish.EXTRA_APP_LABEL, label);
        broadcastIntent.putExtra(UninstallFinish.EXTRA_UNINSTALL_ID, uninstallId);

        PendingIntent pendingIntent =
                PendingIntent.getBroadcast(this, uninstallId, broadcastIntent,
                        PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationManager notificationManager = getSystemService(NotificationManager.class);
        NotificationChannel uninstallingChannel = new NotificationChannel(UNINSTALLING_CHANNEL,
                getString(R.string.uninstalling_notification_channel),
                NotificationManager.IMPORTANCE_MIN);
        notificationManager.createNotificationChannel(uninstallingChannel);

        Notification uninstallingNotification =
                (new Notification.Builder(this, UNINSTALLING_CHANNEL))
                .setSmallIcon(R.drawable.ic_remove).setProgress(0, 1, true)
                .setContentTitle(getString(R.string.uninstalling_app, label)).setOngoing(true)
                .build();

        notificationManager.notify(uninstallId, uninstallingNotification);

        try {
            Log.i(TAG, "Uninstalling extras=" + broadcastIntent.getExtras());

            int flags = mDialogInfo.allUsers ? PackageManager.DELETE_ALL_USERS : 0;
            flags |= keepData ? PackageManager.DELETE_KEEP_DATA : 0;

            ActivityThread.getPackageManager().getPackageInstaller().uninstall(
                    new VersionedPackage(mDialogInfo.appInfo.packageName,
                            PackageManager.VERSION_CODE_HIGHEST),
                    getPackageName(), flags, pendingIntent.getIntentSender(),
                    mDialogInfo.user.getIdentifier());
        } catch (Exception e) {
            notificationManager.cancel(uninstallId);

            Log.e(TAG, "Cannot start uninstall", e);
            showGenericError();
        }
   }

到这里就完成我们的需求了,之后再由PackageInstaller里面的IPackageInstaller的uninstall方法完成具体的卸载操作。

简单实现

基于以上的了解,相信大家对apk的安装和卸载都有了一定的认知,陆游的《冬夜读书示子聿》说到一句:纸上得来终觉浅,绝知此事要躬行。很多时候我们看似会了,但不实际动手操作的话很难进一步的了解,不实践永远也不知道坑有多少,所以就一起来动手实现一个安装和卸载的功能吧。


import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.VersionedPackage;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.util.Log;

import androidx.core.content.FileProvider;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Copyright 王字旁的理
 * Date: 2022/8/18
 * Description: 静默安装与卸载
 * Author: zl
 */
public class InstallUtil {

    private static final String TAG = "Install";

    /**
     * 安装apk
     * @param context
     * @param filePath
     */
    public static synchronized void installApk(Context context, String filePath) {
        if (!isSystemSign(context)) {
            Log.e(TAG, "apk不具备系统签名,无法使用静默安装功能!");
            install(context, filePath);
            return;
        }
        File apkFile = new File(filePath);
        Log.e(TAG, "apkPath " + apkFile.getAbsolutePath());
        if (!apkFile.exists()) {
            Log.e(TAG, "apk 不存在!");
            return;
        }
        //1. 获取包安装程序
        PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
        //2. 安装参数
        PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        //设置大小
        sessionParams.setSize(apkFile.length());
        //3. 会话id
        int sessionId = createSession(packageInstaller, sessionParams);
        Log.e(TAG, "sessionId " + sessionId);
        if (sessionId != -1) {
            //4. 将数据拷贝进session
            boolean copySuccess = copyInstallFile(packageInstaller, sessionId, filePath);
            Log.e(TAG, "copySuccess " + copySuccess);
            if (copySuccess) {
                //5. 执行安装
                execInstallCommand(context, packageInstaller, sessionId);
            }
        }
    }

    /**
     * 显示安装
     *
     * @param context
     * @param filePath
     */
    public static synchronized void install(Context context, String filePath) {
        File apkFile = new File(filePath);
        Log.e(TAG, "apkPath " + apkFile.getAbsolutePath());
        if (!apkFile.exists()) {
            Log.e(TAG, "apk 不存在!");
            return;
        }
        Intent installApkIntent = new Intent();
        installApkIntent.setAction(Intent.ACTION_VIEW);
        installApkIntent.addCategory(Intent.CATEGORY_DEFAULT);
        installApkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        //这里只适配了8.0需要有权限
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            boolean hasInstallPermission = context.getPackageManager().canRequestPackageInstalls();
            if (hasInstallPermission) {
                //通过FileProvider赋予apk访问权限
                Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", apkFile);
                installApkIntent.setDataAndType(uri, "application/vnd.android.package-archive");
                installApkIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                if (context.getPackageManager().queryIntentActivities(installApkIntent, 0).size() > 0) {
                    context.startActivity(installApkIntent);
                }
            }
        }
    }

    /**
     * 卸载apk
     *
     * @param context
     * @param packageName
     */
    public static synchronized void uninstallPackage(Context context, String packageName) {
        if (!isSystemSign(context)) {
            Log.e(TAG, "apk不具备系统签名,无法使用静默安装功能!");
            uninstall(context, packageName);
            return;
        }
        Intent intent = new Intent(context, UninstallResultReceiver.class);
        intent.setAction(PackageInstaller.EXTRA_STATUS);
        //创建卸载广播意图
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_MUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
        //获取安装程序
        PackageInstaller installer = context.getPackageManager().getPackageInstaller();
        //执行卸载操作
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            //卸载最高版本apk
            installer.uninstall(new VersionedPackage(packageName, PackageManager.VERSION_CODE_HIGHEST), pendingIntent.getIntentSender());
        } else {
            //卸载apk
            installer.uninstall(packageName, pendingIntent.getIntentSender());
        }
    }

    /**
     * 显式卸载
     *
     * @param context
     * @param packageName
     */
    public static synchronized void uninstall(Context context, String packageName) {
        //获取删除包名的URI
        Uri uri = Uri.parse("package:" + packageName);
        Intent intent = new Intent();
        //设置我们要执行的卸载动作
        intent.setAction(Intent.ACTION_DELETE);
        //设置获取到的URI
        intent.setData(uri);
        context.startActivity(intent);
    }


    /**
     * 创建sessionId
     *
     * @param packageInstaller
     * @param sessionParams
     * @return
     */
    private static int createSession(PackageInstaller packageInstaller, PackageInstaller.SessionParams sessionParams) {
        int sessionId = -1;
        try {
            //根据sessionParams创建sessionId
            sessionId = packageInstaller.createSession(sessionParams);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sessionId;
    }

    /**
     * 拷贝apk文件,写入PackageInstaller.Session
     *
     * @param packageInstaller
     * @param sessionId
     * @param apkFilePath
     * @return
     */
    private static boolean copyInstallFile(PackageInstaller packageInstaller, int sessionId, String apkFilePath) {
        InputStream in = null;
        OutputStream out = null;
        PackageInstaller.Session session = null;
        boolean success = false;
        try {
            File apkFile = new File(apkFilePath);
            //通过sessionId获取PackageInstaller.Session
            session = packageInstaller.openSession(sessionId);
            //打开输入流
            out = session.openWrite("base.apk", 0, apkFile.length());
            //创建文件流
            in = new FileInputStream(apkFile);
            int total = 0, c;
            byte[] buffer = new byte[1024 * 1024];
            //读取文件流
            while ((c = in.read(buffer)) != -1) {
                total += c;
                out.write(buffer, 0, c);
            }
            //同步数据
            session.fsync(out);
            Log.i(TAG, "streamed " + total + " bytes");
            success = true;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (session != null) {
                session.close();
            }
        }
        return success;
    }

    /**
     * 执行安装
     *
     * @param context
     * @param packageInstaller
     * @param sessionId
     * @return
     */
    private static void execInstallCommand(Context context, PackageInstaller packageInstaller, int sessionId) {
        PackageInstaller.Session session = null;
        try {
            //通过sessionId获取PackageInstaller.Session
            session = packageInstaller.openSession(sessionId);
            //创建一个广播意图
            Intent intent = new Intent(context, InstallResultReceiver.class);
            intent.setAction(PackageInstaller.EXTRA_STATUS);

            //设置广播接受者
            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 1, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
            //执行安装命令,安装完成将发送广播通知
            session.commit(pendingIntent.getIntentSender());

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (session != null) {
                session.close();
            }
        }
    }

    /**
     * 根据包名判断app是否具有系统签名
     */
    private static boolean isSystemSign(Context context) {
        return context.getPackageManager().checkSignatures(Binder.getCallingUid(), android.os.Process.SYSTEM_UID) == PackageManager.SIGNATURE_MATCH;
    }

}

总结代码里面都有注解,写的也还是比较详细,希望对大家有用,代码没有做Android版本适配,我这边测试是基于Android12虚拟机测试的,虚拟机用的是原生系统,所以也可去下载Google源码仓库里下载对应的platform.pk8和platform.x509.pem这两个文件,源码仓库链接直达: google源码仓库 ,下载完之后制作签名文件放入apk,打包时使用系统签名,生成的包就具有系统权限了。要是各位不想浪费时间和C币购的话大家也可以用我打包好的,直达链接: Android12系统签名 备注有使用方式,一目了然。

结语

如果你觉得文章还不错就点个免费的小爱心吧,后续有时间还会继续分享各个系列的知识点,感兴趣的话点个关注吧!
如果你有更好的提议欢迎评论区留言或者私信!

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐