服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

基于Android 13:包管理机制详解

日期: 来源:郭霖收集编辑:小余的自习室


/   今日科技快讯   /

当地时间2月22日,谷歌宣布在量子计算机的纠错方面取得了突破,这一研究结果已发表在英国《自然》杂志上。在多数情况下,量子计算机很容易犯错。这是因为量子比特(量子位)依赖的量子态只能维持不到一秒钟。这意味着,计算机还没来得及完成计算,量子系统中编码的信息就很可能已经丢失了。

/   作者简介   /

明天周六,照常休息,祝大家周末愉快!

本篇文章转自小余的自习室的博客,文章主要通过源码分析了第三方应用的安装过程,相信会对大家有所帮助!

原文地址:
https://juejin.cn/post/7180373976089100344

/   前言   /

之前文章我们讲解了PKMS的启动过程。PKMS启动过程中主要做了以下事情:

1. 会对某些配置文件进行解析扫描,放到PKMS对象内存中
2. 会对系统中的应用包括:overlay,system,vendor,app等路径下的应用进行扫描,如果发现有版本更新,则进行应用更新操作。
3. 初始化包管理过程中需要使用到一些环境对象等。

接下面我们再来讲解下第三方应用的安装过程

/   应用安装过程   /

应用安装的方式有如下几种:

普通安装方式

在7.0之后,为了进一步提升文件读写的安全性,Android框架执行的StrictMode API政策禁止在您的应用外部公开file://URI。如果一项包含文件URI的intent离开您的应用,则应用出现故障,并出现FileUriExposedException异常。

这个时候需要使用FileProvider来授权外部文件读写权限

FileProvider

具体使用方式如下:

1. 在AndroidManifest文件中定义:

<provider
    android:authorities="${applicationId}.fileprovider"
    android:name="androidx.core.content.FileProvider"
    android:exported="false"
    android:grantUriPermissions="true"
    >
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/update_files"
        />
</provider>

2.在xml中定义文件update_files.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path
        name="external_storage_install"
        path="yuhb/install"/>
</paths>

3.在代码中调用

/**
普通应用安装方式
7.0以后需要使用FileProvider进行申请
@param apkFile
@param context
*/
public static void generateInstall(File apkFile, Context context){
    if(!apkFile.exists()){
        return;
    }
    Intent intent = getInstallIntent(apkFile, context);
    context.startActivity(intent);
}
//获取安装应用的Intent
private static Intent getInstallIntent(File apkFile, Context context) {
    Uri data;
    Intent intent = new Intent(Intent.ACTION_VIEW);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    data = getInstallUri(context,apkFile);
    //7.0以后使用FileProvider处理
    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//授权其他应用的读权限
        intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);//防止app加固下出现授权失败情况
    //            intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);//授权其他应用写权限
    }
    intent.setDataAndType(data,"application/vnd.android.package-archive");
    return intent;
}
//获取安装文件的uri
private static Uri getInstallUri(Context context,File apkFile) {
    Uri data;
    //7.0以后使用FileProvider处理
    if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
        data = FileProvider.getUriForFile(context,context.getPackageName()+".fileprovider",apkFile);
    }else {
        data = Uri.fromFile(apkFile);
    }
    return data;
}

静默安装方式(需要有root权限)

你是不是尝试了N种方法,打了N个debug,然后得到的却是各种各样的安装失败 ~ 首先类似静默功能一般是被系统所禁止的,只有厂商在自已平台才会开发权限(好比小米的系统应用,默认实现了静默功能,但是如果小米应用移植到vivo就无效了)。

具体使用方式如下:

/**静默安装方式,一般需要root权限或者是厂商自己的系统应用。
@param context
@param apkFilePath
*/
public static void silenceInstallApk(Context context,String apkFilePath) {
    /*apkFilePath:这里我们首先传入的是安装包的路径   installObserver:自定义安装的回调,不需要可以删了*/
    File apkFile = new File(apkFilePath);
    //判断路径下的文件是否存在
    if (!apkFile.exists()) {
        Log.e(TAG, "apkFile is null...");
        return;
    }
    String packageName = "";
    //获取安装包的信息
    PackageInfo packageInfo = context.getPackageManager().getPackageArchiveInfo(apkFilePath,
            PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
    if (packageInfo != null) {
        packageName = packageInfo.packageName;
        String versionName = packageInfo.versionName;
    }
    //获取packageInstaller,后面用来创建PackageInstaller.Session
    PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
    //获取创建PackageInstaller.Session的参数
    PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
            PackageInstaller.SessionParams.MODE_FULL_INSTALL);
    /*指示将在此会话中交付的所有APK的总大小(以字节为单位),系统可以使用它来确保在继续之前存在足够的磁盘空间,
    或者估计安装在外部存储上的容器大小*/
    sessionParams.setSize(apkFile.length());
    PackageInstaller.Session session = null;
    try {
        //代表一个session的唯一ID,这里我是在全局变量中声明,因为后面的另外一个方法用到了这个sessionId
        int mSessionId = packageInstaller.createSession(sessionParams);
        if (mSessionId != -1) {
            //也就是在这个外部的onTransfesApkFile()方法中,将会用到sessionId
            boolean copySuccess = onTransfesApkFile(mSessionId,context,apkFilePath, packageName);
            if (copySuccess) {
                session = context.getPackageManager().getPackageInstaller().openSession(mSessionId);
                //设置安装完成后需要发送的一个自定义安装结果广播,这里我设置了App的NAME,VERSION,PACKAGE
                Intent intent = new Intent(context,
                        InstallResultReceiver.class);
                intent.setAction(PackageInstaller.EXTRA_STATUS);
                intent.putExtra("APP_VERSION", "1.0");
                intent.putExtra("APP_PACKAGE", "com.allinpay.manager");
                //执行结束后,发送intent
                PendingIntent pendingIntent = PendingIntent.getBroadcast(context,1,
                        intent, PendingIntent.FLAG_UPDATE_CURRENT);
                //这里最终进行session的提交
                session.commit(pendingIntent.getIntentSender());
            } else {
                //此处是安装失败的回调,不需要可以删除
    //                    if (installObserver != null) {
    //                        installObserver.observer(false, apkFilePath, packageName);
    //                    }
            }
        }
    } catch (Exception exception) {
        Log.e(TAG, "installApk exception = " + exception.getLocalizedMessage());
    } finally {
        if (null != session) {
            session.close();
        }
        //安装完成需要删除文件
        if (apkFile != null && apkFile.exists()) {
    //                apkFile.delete();
        }
    }
}

private static boolean onTransfesApkFile(int mSessionId,Context context,String apkFilePath, String packageName) {
    InputStream in = null;
    OutputStream out = null;
    PackageInstaller.Session session = null;
    boolean success = false;
    try {
        File apkFile = new File(apkFilePath);
        //根据sessionId来获取其代表的session
        session = context.getPackageManager().getPackageInstaller().openSession(mSessionId);
        //向session中写入文件数据
        out = session.openWrite(packageName + "_base.apk", 0, apkFile.length());
        in = new FileInputStream(apkFile);
        int total = 0;
        int len;
        byte[] buffer = new byte[1024];
        while ((len = in.read(buffer)) != -1) {
            total += len;
            out.write(buffer, 0, len);
        }
        session.fsync(out);
        success = true;
    } catch (IOException exception) {
        exception.printStackTrace();
    } finally {
        if (null != session) {
            session.close();
        }
        try {
            if (null != out) {
                out.close();
            }
            if (null != in) {
                in.close();
            }
        } catch (IOException exception) {
            exception.printStackTrace();
        }
    }
    return success;
}

这里我们来分析下方式一(普通应用更新方式

1. 提取关键代码:调用的startActivity中的Intent属性

Action:Intent.ACTION_VIEW
Flag:Intent.FLAG_ACTIVITY_NEW_TASK
Uri:content格式
Type:application/vnd.android.package-archive

2. 根据以上Intent,调用了startActivity,然后通过PKMS找到对应的Activity。最终定位到:InstallStart类,这个类就是启动安装时打开的第一个Activity

/**
Select which activity is the first visible activity of the installation and forward the intent to it.
*/
public class InstallStart extends Activity {
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        ...
        Uri packageUri = intent.getData();
        //如果Scheme是Content格式
        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);
    //如果Scheme是package格式
    } else if (packageUri != null && packageUri.getScheme().equals(
            PackageInstallerActivity.SCHEME_PACKAGE)) {
        nextActivity.setClass(this, PackageInstallerActivity.class);
    //如果Scheme是其他格式
    } 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);
    }
    finish();
    ```
}

InstallStart的onCreate方法会对传入的Scheme格式进行判断,然后启动另外一个Activity,并结束自己。我们重点来看Content格式的Activity。最终启动的是InstallStaging.class。看父类名字应该是一个选择框类型的Activity。

//frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/InstallStaging.java
public class InstallStaging extends AlertActivity {
    protected void onResume() {
        ...
        mStagingTask = new StagingAsyncTask();
        //执行StagingAsyncTask
        mStagingTask.execute(getIntent().getData());
    }


    private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {
        @Override
        protected Boolean doInBackground(Uri... params) {
            if (params == null || params.length <= 0) {
                return false;
            }
            Uri packageUri = params[0];
            try (InputStream in = getContentResolver().openInputStream(packageUri)) {
                // Despite the comments in ContentResolver#openInputStream the returned stream can
                // be null.
                if (in == null) {
                    return false;
                }

                try (OutputStream out = new FileOutputStream(mStagedFile)) {
                    byte[] buffer = new byte[1024 * 1024];
                    int bytesRead;
                    while ((bytesRead = in.read(buffer)) >= 0) {
                        // Be nice and respond to a cancellation
                        if (isCancelled()) {
                            return false;
                        }
                        out.write(buffer, 0, bytesRead);
                    }
                }
            } catch (IOException | SecurityException | IllegalStateException e) {
                Log.w(LOG_TAG, "Error staging apk from content URI", e);
                return false;
            }
            return true;
        }

        @Override
        protected void onPostExecute(Boolean success) {
            if (success) {
                // Now start the installation again from a file
                Intent installIntent = new Intent(getIntent());
                installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class);
                installIntent.setData(Uri.fromFile(mStagedFile));

                if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
                    installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
                }

                installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
                startActivity(installIntent);

                InstallStaging.this.finish();
            } else {
                showError();
            }
        }
    }

}

1. InstallStaging的onResume时会启用了一个ASyncTask,在后台读取apk文件,并写入到mStagedFile文件中。mStagedFile文件的作用是临时文件,防止在安装过程中对原文件变更。

2. 在文件读取完成后,调用AsyncTask的onPostExecute方法,这个方法中会再次启动一个DeleteStagedFileOnResult类Activity。

继续进入DeleteStagedFileOnResult

/**
 * Trampoline activity. Calls PackageInstallerActivity and deletes staged install file onResult.
   */
   public class DeleteStagedFileOnResult extends Activity {
   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       if (savedInstanceState == null) {
           Intent installIntent = new Intent(getIntent());
           installIntent.setClass(this, PackageInstallerActivity.class);

           installIntent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
           startActivityForResult(installIntent, 0);
       }

}

看谷歌给我们的注解:这个类是一个过渡Activity:最终是用来启动PackageInstallerActivity并删除mStagedFile临时文件,这在onCreate方法中也可以看出。

那就转到PackageInstallerActivity,在PackageInstallerActivity中会让引导用户点击安装按钮,点击之后会调用startInstall方法进行安装操作。

//frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java
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();
}

startInstall重新启动了一个“InstallInstalling”去安装,并将启动应用需要的参数信息放到Intent中。

//frameworks/base/packages/PackageInstaller/src/com/android/packageinstaller/InstallInstalling.java

public class InstallInstalling extends AlertActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
                        PackageInstaller.SessionParams.MODE_FULL_INSTALL);
        ...
        //注册一个安装结果监听器launchFinishBasedOnResult
        mInstallId = InstallEventReceiver
                            .addObserver(this, EventResultPersister.GENERATE_NEW_ID,
                                    this::launchFinishBasedOnResult);
        ...
        //创建一个createSession
        mSessionId = getPackageManager().getPackageInstaller().createSession(params);
    }


    @Override
    protected void onResume() {
        //启动一个InstallingAsyncTask
        mInstallingTask = new InstallingAsyncTask();
        mInstallingTask.execute();
    }

    private final class InstallingAsyncTask extends AsyncTask<Void, Void,
        PackageInstaller.Session> {

        @Override
        protected PackageInstaller.Session doInBackground(Void... params) {
            PackageInstaller.Session session;
            try {
                //打开Session
                session = getPackageManager().getPackageInstaller().openSession(mSessionId);
            } catch (IOException e) {

            }
            //设置session安装进度
            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) {

            }
        }

        @Override
        protected void onPostExecute(PackageInstaller.Session session) {
            if (session != null) {
                //注册一个broadcastIntent监听安装结果:BROADCAST_ACTION = "com.android.packageinstaller.ACTION_INSTALL_COMMIT";
                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);
                //调用commit进行安装
                session.commit(pendingIntent.getIntentSender());
                mCancelButton.setEnabled(false);
                setFinishOnTouchOutside(false);
            } 
        }
    }

}
}

InstallInstalling可以总结为下面几个步骤:

1. 创建session
2. 打开session
3. copy apk文件到Session中
4. 调用commit进行安装。

仔细观察你会发现:这里步骤和我们前面分析的静默安装方式步骤其实是一样的。而我们的InstallInstalling是运行在系统进程中,所以拥有静默安装权限, 而第三方应用是没有这个权限的

下面我们深入PackageInstaller看看其是如何实现安装过程的?

首先来看context.getPackageManager().getPackageInstaller()获取到的是哪个类?

如果你对Activity熟悉的话,应该知道context的实现类是ContextImpl类。

定位到它的getPackageManager。

//frameworks/base/core/java/android/app/ContextImpl.java
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }

    final IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }

    return null;

}

由此可知getPackageManager返回的是一个ApplicationPackageManager,而这里有个关键参数pm,后期操作实际都是通过pm进行的。pm是通过ActivityThread.getPackageManager()获取。

//frameworks/base/core/java/android/app/ActivityThread.java
public static IPackageManager getPackageManager() {
    if (sPackageManager != null) {
        return sPackageManager;
    }
    final IBinder b = ServiceManager.getService("package");
    sPackageManager = IPackageManager.Stub.asInterface(b);
    return sPackageManager;
}

哦?原来就是获取ServiceManager中的package服务。如果你还有印象,前面我们分析过在PKMS的main方法中有下面这段代码。

//构造IPackageManagerImpl对象并将其add到ServiceManager中:name为package
IPackageManagerImpl iPackageManager = m.new IPackageManagerImpl();
ServiceManager.addService("package", iPackageManager);

所以这里返回的是一个IPackageManagerImpl对象。

好了,回到前面context.getPackageManager().getPackageInstaller()

context.getPackageManager():对应ApplicationPackageManager(context,IPackageManagerImpl)

进入ApplicationPackageManager的getPackageInstaller:

//frameworks/base/core/java/android/app/ApplicationPackageManager.java
public PackageInstaller getPackageInstaller() {
    if (mInstaller == null) {
        try {
            mInstaller = new PackageInstaller(mPM.getPackageInstaller(),
                    mContext.getPackageName(), mContext.getAttributionTag(), getUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    return mInstaller;
}

由此可知getPackageInstaller返回的是一个PackageInstaller对象,而关键看第一个参数mPM.getPackageInstaller(),这个参数也是实际进行安装的类。

前面分析过mPM是IPackageManagerImpl对象,进入IPackageManagerImpl.getPackageInstaller()

在IPackageManagerImpl父类IPackageManagerBase实现了getPackageInstaller

//frameworks/base/services/core/java/com/android/server/pm/IPackageManagerBase.java
public final IPackageInstaller getPackageInstaller() {
    ...
    return mInstallerService;
}

返回的是一个mInstallerService,这个mInstallerService是在哪里赋值的呢?经过几轮跳转,定位到。

mInstallerService是在PKMS的构造方法中赋值的:mInstallerService = mInjector.getPackageInstallerService();

mInjector是PackageManagerServiceInjector类(PKMS的运行时环境类)。

最终获取的mInstallerService是在PKMS构造过程中也就是系统开机时初始化的PackageInstallerService对象。

好了这里画了一张图来表示他们之间的关系:


由以上分析可知:context.getPackageManager().getPackageInstaller()获取的是PackageInstaller,而实际安装操作是PackageInstaller的内部PackageInstallerService对象

下面我们根据前面分析出的安装步骤进行具体分析 1.创建session 2.打开session 3.copy apk文件到Session中 4.调用commit进行安装。

创建session

//frameworks/base/core/java/android/content/pm/PackageInstaller.java
public int createSession(@NonNull SessionParams params) throws IOException {
    //mInstaller为PackageInstallerService
    return mInstaller.createSession(params, mInstallerPackageName, mAttributionTag,
            mUserId);
}
PackageInstallerService的createSession方法会调用内置的createSessionInternal方法
private int createSessionInternal(SessionParams params, String installerPackageName...){
    //前面一大堆对Session创建的条件进行判断,不满足创建则抛出异常
    //创建随机数的sessionId
    sessionId = allocateSessionIdLocked();
    //创建SessionDir
    stageDir = buildSessionDir(sessionId, params);
    //InstallSource持有应用安装的apk源文件信息
    InstallSource installSource = InstallSource.create(installerPackageName,
                originatingPackageName, requestedInstallerPackageName,
                installerAttributionTag, params.packageSource);
    //创建session :PackageInstallerSession
    session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this,
            mSilentUpdatePolicy, mInstallThread.getLooper(), mStagingManager, sessionId,
            userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid,
            null, null, false, false, false, false, null, SessionInfo.INVALID_ID,
            false, false, false, PackageManager.INSTALL_UNKNOWN, "");
    //将session放入到mSessions键值对中,key为sessionId
    synchronized (mSessions) {
        mSessions.put(sessionId, session);
    }
    //将InstallSource放入到PKMS的Setting集合中
    mPm.addInstallerPackageName(session.getInstallSource());
}

createSessionInternal过程主要就是创建了PackageInstallerSession对象,并将对象放入到mSessions集合中。

打开session

打开Session也是调用PackageInstallerService的createSession方法,内部调用openSessionInternal进行打开。

private IPackageInstallerSession openSessionInternal(int sessionId) throws IOException {
    synchronized (mSessions) {
        final PackageInstallerSession session = mSessions.get(sessionId);
        if (!checkOpenSessionAccess(session)) {
            throw new SecurityException("Caller has no access to session " + sessionId);
        }
        session.open();
        return session;
    }
}

内容很简单,通过sessionId去mSessions获取Session对象,然后调用session.open()打开。

//frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java
public void open() throws IOException {
    ...
    boolean wasPrepared;
    synchronized (mLock) {
        wasPrepared = mPrepared;
        if (!mPrepared) {
            if (stageDir != null) {
                prepareStageDir(stageDir);
            }
            mPrepared = true;
        }
    }
}
static void prepareStageDir(File stageDir) throws IOException {


    try {
        Os.mkdir(stageDir.getAbsolutePath(), 0775);
        Os.chmod(stageDir.getAbsolutePath(), 0775);
    } catch (ErrnoException e) {

    }

}

open方法也只是调用Os的mkdir进行stageDir文件夹创建,并且给stageDir文件夹设置了对应的权限、 stageDir临时文件夹路径:new File("data/app", "vmdl" + sessionId + ".tmp");

copy apk文件到Session中

Session文件的写操作openWrite最终调用的是PackageInstallerSession的doWriteInternal,写文件就不介绍了,这个大家都非常清楚了。只要知道文件是写入到的是stageDir临时文件夹("data/app/vmdl{$sessionId}.tmp")下面

调用commit进行安装

public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) {
    ...
    dispatchSessionSealed();
}
private void dispatchSessionSealed() {
    mHandler.obtainMessage(MSG_ON_SESSION_SEALED).sendToTarget();
}

private final Handler.Callback mHandlerCallback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {


            case MSG_ON_SESSION_SEALED:
                //内部发射一个MSG_STREAM_VALIDATE_AND_COMMIT msg
                handleSessionSealed();
                break;
            case MSG_STREAM_VALIDATE_AND_COMMIT:
                //内部发射一个MSG_INSTALL msg
                handleStreamValidateAndCommit();
                break;
            case MSG_INSTALL:
                //处理应用安装过程
                handleInstall();
                break;
            case MSG_ON_PACKAGE_INSTALLED:
                final SomeArgs args = (SomeArgs) msg.obj;
                final String packageName = (String) args.arg1;
                final String message = (String) args.arg2;
                final Bundle extras = (Bundle) args.arg3;
                final IntentSender statusReceiver = (IntentSender) args.arg4;
                final int returnCode = args.argi1;
                args.recycle();

                sendOnPackageInstalled(mContext, statusReceiver, sessionId,
                        isInstallerDeviceOwnerOrAffiliatedProfileOwner(), userId,
                        packageName, returnCode, message, extras);

                break;
            case MSG_SESSION_VALIDATION_FAILURE:
                final int error = msg.arg1;
                final String detailMessage = (String) msg.obj;
                onSessionValidationFailure(error, detailMessage);
                break;
        }

        return true;
    }

};

发送MSG_ON_SESSION_SEALED的msg调用handleSessionSealed方法。handleSessionSealed方法内部又发送了MSG_STREAM_VALIDATE_AND_COMMIT的msg。然后在handleStreamValidateAndCommit又发送了MSG_INSTALL。所以最终调用了handleInstall方法进行安装。

handleInstall方法可以大致分为:

1. apk文件的校验
2. apk文件的安装

@WorkerThread
private void handleInstall() {
    ...
    if (params.isStaged) {
        mStagedSession.verifySession();
    } else {
        verify();
    }
}

mStagedSession.verifySession最终也会走到verify,可以直接看verify方法

private void verify() {
    try {
        //1.创建安装apk需要的文件夹:
        prepareInheritedFiles();
        //2.解析APK文件并提取so库文件。其实就是解析AndroidManfest中的四大组件信息
        parseApkAndExtractNativeLibraries();
        //3.检验apk的过程
        verifyNonStaged();
    } catch (PackageManagerException e) {
        final String completeMsg = ExceptionUtils.getCompleteMessage(e);
        final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg);
        setSessionFailed(e.error, errorMsg);
        onSessionVerificationFailure(e.error, errorMsg);
    }
}

重点来看3.检验apk的过程,verifyNonStaged在经过一系列session检查之后,最终会调用到PackageSessionVerifier的verifyAPK方法, verifyAPK内部设置了安装结果监听IPackageInstallObserver2:

//frameworks/base/services/core/java/com/android/server/pm/PackageSessionVerifier.java
private void verifyAPK(PackageInstallerSession session, Callback callback)
            throws PackageManagerException {
    final IPackageInstallObserver2 observer = new IPackageInstallObserver2.Stub() {
        @Override
        public void onUserActionRequired(Intent intent) {
            throw new IllegalStateException();
        }
        @Override
        public void onPackageInstalled(String basePackageName, int returnCode, String msg,
                Bundle extras) {
            if (session.isStaged() && returnCode == PackageManager.INSTALL_SUCCEEDED) {
                // Continue verification for staged sessions
                verifyStaged(session.mStagedSession, callback);
                return;
            }
            if (returnCode != PackageManager.INSTALL_SUCCEEDED) {
                String errorMessage = PackageManager.installStatusToString(returnCode, msg);
                session.setSessionFailed(returnCode, errorMessage);
                callback.onResult(returnCode, msg);
            } else {
                session.setSessionReady();
                callback.onResult(PackageManager.INSTALL_SUCCEEDED, null);
            }
        }
    };
    final VerificationParams verifyingSession = makeVerificationParams(session, observer);
    ...
    verifyingSession.verifyStage();

}
frameworks/base/services/core/java/com/android/server/pm/VerificationParams.java
public void verifyStage() {
    mPm.mHandler.post(this::startCopy);
}

可以看到verifyStage最终调用了mPm.mHandler post了一个startCopy的任务。

final void startCopy() {
    handleStartCopy();
    handleReturnCode();
}

handleStartCopy和handleReturnCode是两个抽象方法:具体实现是在VerificationParams类中

public void handleStartCopy() {
    //获取需要安装的apk包信息
    PackageInfoLite pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mPm.mContext,
            mPackageLite, mOriginInfo.mResolvedPath, mInstallFlags, mPackageAbiOverride);
    //校验需要更新的app的VersionCode,这里面会对VersionCode版本和原始版本进行校验。
    Pair<Integer, String> ret = mInstallPackageHelper.verifyReplacingVersionCode(
            pkgLite, mRequiredInstalledVersionCode, mInstallFlags);


    if (!mOriginInfo.mExisting) {
        //如果是PackageManager.INSTALL_APEX不是APEX包,也就是apk包,则调用sendApkVerificationRequest对APK包进行更新
        if ((mInstallFlags & PackageManager.INSTALL_APEX) == 0) {   
            sendApkVerificationRequest(pkgLite);//关注点1
        }
        //回溯版本走的
        if ((mInstallFlags & PackageManager.INSTALL_ENABLE_ROLLBACK) != 0) {
            sendEnableRollbackRequest();
        }
    }

}

//关注点1
private void sendApkVerificationRequest(PackageInfoLite pkgLite) {
    ...
    //发送完整的校验请求
    sendIntegrityVerificationRequest(verificationId, pkgLite, verificationState);
    //发送package安装包校验
    sendPackageVerificationRequest(
            verificationId, pkgLite, verificationState);
    ...
}

sendPackageVerificationRequest主要校验下面几个:

1. 四大组件包信息校验
2. apk打包公钥校验
3. 校验打包的sdk版本信息,通过添加广播的方式进行。

好了,继续回到startCopy的handleReturnCode方法

//frameworks/base/services/core/java/com/android/server/pm/VerificationParams.java
void handleReturnCode() {
    sendVerificationCompleteNotification();
}
private void sendVerificationCompleteNotification() {
    ...
    try {
        mObserver.onPackageInstalled(null, mRet, mErrorMessage,
                new Bundle());
    } catch (RemoteException e) {
        Slog.i(TAG, "Observer no longer exists.");
    }
    ...
}

在校验完毕成功以后,回调mObserver的onPackageInstalled方法。而mObserver之前说过是在verifyAPK方法时传入的。

final IPackageInstallObserver2 observer = new IPackageInstallObserver2.Stub() {


    @Override
    public void onPackageInstalled(String basePackageName, int returnCode, String msg,
            Bundle extras) {
        ...
        if (returnCode != PackageManager.INSTALL_SUCCEEDED) {
            String errorMessage = PackageManager.installStatusToString(returnCode, msg);
            session.setSessionFailed(returnCode, errorMessage);
            callback.onResult(returnCode, msg);
        } else {
            session.setSessionReady();
            callback.onResult(PackageManager.INSTALL_SUCCEEDED, null);
        }
    }

};

onPackageInstalled回调成功后会再次调用callback的onResult方法

callBack是在前面分析的PackageInstallerSession的verifyNonStaged方法中传入的,一层一层向外回调。

private void verifyNonStaged()
        throws PackageManagerException {


    mSessionProvider.getSessionVerifier().verify(this, (error, msg) -> {
        mHandler.post(() -> {
            if (dispatchPendingAbandonCallback()) {
                // No need to continue if abandoned
                return;
            }
            if (error == INSTALL_SUCCEEDED) {
                onVerificationComplete();
            } else {
                onSessionVerificationFailure(error, msg);
            }
        });
    });

}

最终回调到onVerificationComplete方法,可以看到前面很大一部分是在对应用进行校验的部分。

下面分析的才是具体安装的过程:

@WorkerThread
private void onVerificationComplete() {
    ...
    install();
}
private CompletableFuture<Void> install() {
    List<CompletableFuture<InstallResult>> futures = installNonStaged();
    ...
}

private List<CompletableFuture<InstallResult>> installNonStaged() {
    ... 
    final InstallParams installingSession = makeInstallParams(future);  
    installingSession.installStage();
    ...
}

frameworks/base/services/core/java/com/android/server/pm/InstallParams.java
public void installStage() {
    final Message msg = mPm.mHandler.obtainMessage(INIT_COPY);


    msg.obj = this;
    mPm.mHandler.sendMessage(msg);

}

installStage发送了一个INIT_COPY的msg,定位到mPm = PackageManagerImpl.java mPm.mHandler = PackageHandler

final class PackageHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        try {
            doHandleMessage(msg);
        } finally {
            Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
        }
    }

    void doHandleMessage(Message msg) {
        switch (msg.what) {
            case INIT_COPY: {
                HandlerParams params = (HandlerParams) msg.obj;
                if (params != null) {
                    ...
                    params.startCopy();                
                }
                break;
            }
        }
    }

}

INIT_COPY的msg调用了HandlerParams的startCopy方法处理,而这个时候的HandlerParams的实现类是InstallParams.java,前面校验过程中的实现类是VerificationParams

final void startCopy() {
    handleStartCopy();//关注点1
    handleReturnCode();//关注点2
}

先看关注点1

//frameworks/base/services/core/java/com/android/server/pm/InstallParams.java
public void handleStartCopy() {


    PackageInfoLite pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mPm.mContext,
            mPackageLite, mOriginInfo.mResolvedPath, mInstallFlags, mPackageAbiOverride);

    if (!mOriginInfo.mStaged && pkgLite.recommendedInstallLocation
            == InstallLocationUtils.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {
        //先释放一部分不需要的缓存。
        pkgLite.recommendedInstallLocation = mPm.freeCacheForInstallation(
                pkgLite.recommendedInstallLocation, mPackageLite,
                mOriginInfo.mResolvedPath, mPackageAbiOverride, mInstallFlags);
    }
    //根据默认的规则重写安装路径,主要是区分使用外置sdcard路径还是内置路径
    mRet = overrideInstallLocation(pkgLite.packageName, pkgLite.recommendedInstallLocation,
            pkgLite.installLocation);

}

再看关注点2:
void handleReturnCode() {
    processPendingInstall();
}
private void processPendingInstall() {
    //创建安装的参数信息
    InstallArgs args = createInstallArgs(this);
    if (mRet == PackageManager.INSTALL_SUCCEEDED) {
        //关注点3 拷贝apk
        mRet = args.copyApk();
    }
    if (mRet == PackageManager.INSTALL_SUCCEEDED) {
        F2fsUtils.releaseCompressedBlocks(
                mPm.mContext.getContentResolver(), new File(args.getCodePath()));
    }
    if (mParentInstallParams != null) {
        mParentInstallParams.tryProcessInstallRequest(args, mRet);
    } else {


        PackageInstalledInfo res = new PackageInstalledInfo(mRet);
        //关注点4
        processInstallRequestsAsync(
                res.mReturnCode == PackageManager.INSTALL_SUCCEEDED,
                Collections.singletonList(new InstallRequest(args, res)));
    }

}

processPendingInstall关注两个部分:

1.拷贝apk
2.安装apk

1. 拷贝apk:mRet = args.copyApk();

而args 是FileInstallArgs类对象

//frameworks/base/services/core/java/com/android/server/pm/FileInstallArgs.java
int copyApk() {
    return doCopyApk();
}
private int doCopyApk() {
    //1.给StageDir分配对应的临时文件夹以及权限
    final File tempDir = mPm.mInstallerService.allocateStageDirLegacy(mVolumeUuid, isEphemeral);    
    mCodeFile = tempDir;
    //2.拷贝Package,这里面主要是四大组件信息的拷贝
    int ret = PackageManagerServiceUtils.copyPackage(
            mOriginInfo.mFile.getAbsolutePath(), mCodeFile);


    //3.根据abifilter 拷贝NativeLibrary ,so库到对应的lib目录下
    handle = NativeLibraryHelper.Handle.create(mCodeFile);
    ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
            mAbiOverride, isIncremental);

    return ret;

}

先来分析1处allocateStageDirLegacy

public File allocateStageDirLegacy(String volumeUuid, boolean isEphemeral) throws IOException {
    synchronized (mSessions) {
        try {
            final int sessionId = allocateSessionIdLocked();
            mLegacySessions.put(sessionId, true);
            final File sessionStageDir = buildTmpSessionDir(sessionId, volumeUuid);
            prepareStageDir(sessionStageDir);
            return sessionStageDir;
        } catch (IllegalStateException e) {
            throw new IOException(e);
        }
    }
}

看buildTmpSessionDir,这个前面也分析过,最后返回的File路径为:data/app/vmdl{$sessionId}.tmp

再来分析2处PackageManagerServiceUtils.copyPackage

public static int copyPackage(String packagePath, File targetDir) {
    try {
        final File packageFile = new File(packagePath);
        //解析APK文件到ParseResult中
        final ParseResult<PackageLite> result = ApkLiteParseUtils.parsePackageLite(
                input.reset(), packageFile, /* flags */ 0);
        //获取apk的文件PackageLite信息
        final PackageLite pkg = result.getResult();
        //拷贝file,核心方法
        copyFile(pkg.getBaseApkPath(), targetDir, "base.apk");


        return PackageManager.INSTALL_SUCCEEDED;
    } catch (IOException | ErrnoException e) {
    }

}
private static void copyFile(String sourcePath, File targetDir, String targetName)
        throws ErrnoException, IOException {


    final File targetFile = new File(targetDir, targetName);
    final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(),
            O_RDWR | O_CREAT, 0644);
    Os.chmod(targetFile.getAbsolutePath(), 0644);
    FileInputStream source = null;
    try {
        source = new FileInputStream(sourcePath);
        FileUtils.copy(source.getFD(), targetFd);
    } finally {
        IoUtils.closeQuietly(source);
    }

}

copyPackage会先去解析apk文件,然后调用copyFile方法,copyFile中调用Os.open去打开targetFile目标文件, 调用FileUtils.copy方法将原文件拷贝到目标文件中

2.安装apk:processInstallRequestsAsync

// Queue up an async operation since the package installation may take a little while.
private void processInstallRequestsAsync(boolean success,
        List<InstallRequest> installRequests) {
    mPm.mHandler.post(() -> {
        mInstallPackageHelper.processInstallRequests(success, installRequests);
    });
}
frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
public void processInstallRequests(boolean success, List<InstallRequest> installRequests) {


    List<InstallRequest> apkInstallRequests = new ArrayList<>();
    for (InstallRequest request : installRequests) {
        ...
        apkInstallRequests.add(request);

    }

    if (success) {
        for (InstallRequest request : apkInstallRequests) {
            //预安装,内部主要是做clean操作
            request.mArgs.doPreInstall(request.mInstallResult.mReturnCode);
        }
        synchronized (mPm.mInstallLock) {
            //实际安装apk过程
            installPackagesTracedLI(apkInstallRequests);
        }
    }
    ...

}
private void installPackagesTracedLI(List<InstallRequest> requests) {
    ...
    installPackagesLI(requests);
    ...
}

下面我们重点来分析下installPackagesLI

private void installPackagesLI(List<InstallRequest> requests) {


    for (InstallRequest request : requests) {       
        //阶段1:prepare阶段
        repareResult = preparePackageLI(request.mArgs, request.mInstallResult);         
        //阶段2:scan阶段
        final ScanResult result = scanPackageTracedLI(
                prepareResult.mPackageToScan, prepareResult.mParseFlags,
                prepareResult.mScanFlags, System.currentTimeMillis(),
                request.mArgs.mUser, request.mArgs.mAbiOverride);       
    }   
    //阶段3:Reconcile阶段
    reconciledPackages = ReconcilePackageUtils.reconcilePackages(
            reconcileRequest, mSharedLibraries,
            mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
    }

    commitRequest = new CommitRequest(reconciledPackages,
                            mPm.mUserManager.getUserIds());
    //阶段4:Commit阶段
    commitPackagesLocked(commitRequest);
    //阶段5:完成apk安装
    executePostCommitSteps(commitRequest);

}

installPackagesLI是最终安装应用的方法:主要分为4个阶段

阶段1:prepare阶段:分析当前安装包的状态,解析安装包并对其做初始化验证
阶段2:scan阶段:根据prepare阶段中收集的安装包状态信息去扫描解析出来的包
阶段3:Reconcile阶段:验证scan阶段扫描到的Package信息以及当前系统状态,确保apk的正确安装。
阶段4:Commit阶段:提交所有扫描的包并更新系统状态。这是唯一可以在安装流程和所有可预测错误中修改系统状态的地方.

在 preparePackageLI() 内使用 PackageParser2.parsePackage() 解析AndroidManifest.xml,获取四大组件等信息;使用ParsingPackageUtils.getSigningDetails() 解析签名信息;重命名包最终路径 等。

完成了解析和校验准备工作后,最后一步就是对apk的安装了。这里调用了executePostCommitSteps准备app数据,并执行dex优化

最后通过executePostCommitSteps完成apk的安装,执行dex优化等操作

阶段5:完成apk安装

private void executePostCommitSteps(CommitRequest commitRequest) {


    for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {

        // prepareAppDataPostCommitLIF经过一系列调用会走到Installer的createAppData方法。
        mAppDataHelper.prepareAppDataPostCommitLIF(pkg, 0);

        /*
        检测是否需要进行dex优化:同时满足下面三种情况就需要
        1.不是一个即时应用app或者如果是的话通过gservices进行dex优化操作
        2.debuggable为false
        3.不在增量文件系统上。
        */
        final boolean performDexopt =
                (!instantApp || android.provider.Settings.Global.getInt(
                        mContext.getContentResolver(),
                        android.provider.Settings.Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
                        && !pkg.isDebuggable()
                        && (!onIncremental)
                        && dexoptOptions.isCompilationEnabled();

        if (performDexopt) {

            //获取so库所在的目录
            PackageSetting realPkgSetting = result.mExistingSettingCopied
                    ? result.mRequest.mPkgSetting : result.mPkgSetting;

            boolean isUpdatedSystemApp = reconciledPkg.mPkgSetting.getPkgState()
                    .isUpdatedSystemApp();
            //更新系统app信息。
            realPkgSetting.getPkgState().setUpdatedSystemApp(isUpdatedSystemApp);
            //进行dex优化
            mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
                    null /* instructionSets */,
                    mPm.getOrCreateCompilerPackageStats(pkg),
                    mDexManager.getPackageUseInfoOrDefault(packageName),
                    dexoptOptions);

        }

        //通知BackgroundDexOptService服务当前packageName的应用进行了更新。
        BackgroundDexOptService.getService().notifyPackageChanged(packageName);

        notifyPackageChangeObserversOnUpdate(reconciledPkg);
    }

}

阶段5:主要做了下面两件事

任务1:调用prepareAppDataPostCommitLIF方法,最终执行到createAppData方法进行app的安装.

public @NonNull CreateAppDataResult createAppData(@NonNull CreateAppDataArgs args)
    throws InstallerException {
...
try {
    return mInstalld.createAppData(args);
} catch (Exception e) {
    throw InstallerException.from(e);
}
}

mInstalld在前面分析过了,installd进程 的执行权限为 root,所有实际的应用安装,卸载等操作都是通过这个服务进行的。PKMS只是java层的封装。mInstalld进程和PKMS是通过binder进行通讯的。

任务2.调用performDexOpt进行dex优化 同时满足下面三种情况就需要

1.不是一个即时应用app或者如果是的话通过gservices进行dex优化操作
2.debuggable为false
3.不在增量文件系统上。

然后关于dex优化部分,后面会单独出一篇文章来讲解。

关于应用安装部分就讲到这里了。



推荐阅读:
我的新书,《第一行代码 第3版》已出版!
Compose跨平台又来了,这次能开发iOS了
2022年终总结,我的10年Android之旅

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注


相关阅读

  • App Bundle?了解一下!

  • / 今日科技快讯 /近日,美国国家航空航天局表示,波音“星际线”飞船将于今年春季进行首次载人飞行,将首批宇航员送往国际空间站。 据悉,“星际线”的最近一次任务是轨道飞行
  • 分享两种方式:如何开启JNI的“大门”?

  • ‍‍1要介绍本篇博客的原因前段时间学习OpenGL ES相关技术,下载了一个Github项目学习,项目地址在:https://github.com/githubhaohao/NDK_OpenGLES_3_0项目的关键代码都是C++实
  • 祖传代码,业务代码参数透传满天飞?

  • 本文作者作者:唐子玄链接:https://juejin.cn/post/7165427212911378445本文由作者授权发布。1引子项目中参数多级透传满天飞的情况很常见,增加了开发的复杂度、出错的可能、及
  • 封神之作:极致包体优化,解决 17 个业务痛点

  • ‍本文作者:小木箱,原文发布于:小木箱成长营。1引言Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享包体积优化 · 实战论 · 怎么做包体优化? 做好能晋升吗? 能涨多少
  • 量化策略实战:构建交易方法论

  • 大家对量化交易或量化投资的方式都不陌生了,我们已接触和学习的包含但不限于量化投资门类,诸如程序化交易、算法交易、高频交易、套利交易、量化选股、量化择时等种类繁多的量
  • CQF持证人专访 | 三证备考,量化投资上下求索

  • 陆续在做CQF持证人访谈,这次是傅同学,CFA、FRM、CQF持证人。硕士毕业于英国的埃克塞特大学金融经济专业,目前在国内一家大型券商做金融产品及量化研究。平时喜欢旅游、健身,研究
  • 千万别在有WiFi的房间里摆这种姿势,只因……

  • 量子位(ID:QbitAI)明敏 丰色 发自 凹非寺现在,只用WiFi就能“看”到你在房间里干啥了……(你…干…嘛……啊啊啊啊)多人追踪也是so easy:过程中完全不需要拍下图像、不需要摄像头。
  • Linux 中使用 Tee 和 Xargs 从标准输入运行命令

  • 点击上方蓝字 ● 关注Linux公社 使用命令行时,您可以直接将一个程序(例如生成某些系统信息或统计信息的工具)的输出作为另一个程序(例如文本过滤或模式搜索工具,如grep、sed
  • 穿越周期的资产配置逻辑,你必须知道!

  • 最近,在黄金海岸,一栋靠海的44层的超高层公寓项目,第42层,650平米,刚刚售出,卖了1200万澳币。而这套房,原房东是2021年5月买的,原购入价是699万。一年半的时间,净赚了500万澳币!同一栋
  • 收费服务还是来了,不限速的2T空间还在不在?

  • 不星标可能收不到消息,记得星标公众号回复QQ群获取群号前言最近123网盘上线会员服务了,之前一直没有收费功能,所有功能都是免费,上传下载不限速,新用户统一2T空间而几天前,123网盘

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 基于Android 13:包管理机制详解

  • / 今日科技快讯 /当地时间2月22日,谷歌宣布在量子计算机的纠错方面取得了突破,这一研究结果已发表在英国《自然》杂志上。在多数情况下,量子计算机很容易犯错。这是因为量
  • Kotlin协程开发的基础入门知识

  • / 今日科技快讯 /近日,爱奇艺官方账号发文,针对近期爱奇艺VIP会员用户反馈的投屏清晰度、设备登录等问题做出如下调整:1.为2023年2月20日仍处于订阅状态的爱奇艺黄金 VIP
  • 湖北医药学院举行春季招聘会,提供岗位两万余个

  • 十堰广电讯(全媒体记者 翁红 见习记者 钱心玥 通讯员 鲍晓宇)2月28日,湖北医药学院举行春季招聘会,来自省内外700多家医疗卫生机构,提供两万多个岗位,吸引毕业生求职就业。28日上
  • 拥有思想,你就是高级、资深、专家、架构师

  • / 今日科技快讯 /近日,阿里国际站最新跨境指数显示,过去一年新能源车充电桩的海外商机快速增长了245%,而未来还有将近3倍的需求空间,成为国内外贸企业的新机会。据悉,阿里国
  • 平塘公安“天、地”联动 禁种铲毒不留死角

  • 今年以来,平塘县公安局坚持问题导向、精准施策,强化科技支撑、综合治理,从“早部署+广宣传+深踏查”三个维度发力,高效铲除毒品隐患根源,扎实开展禁种铲毒专项行动,确保实现“零种
  • Android自定义通知方方面面全适配

  • / 今日科技快讯 /近日,社交媒体推特老板埃隆·马斯克在回复用户推文时表示,推特将于下周开源算法。在此之前,马斯克一直说要将推特算法开源。周二一名推特用户表示,如果“现