Android Apk安装过程解析

本文主要内容

  • 静默安装
  • apk安装流程简析
  • installd进程意义

最近工作上遇到静默安装相关的内容,顺便学习一下apk安装的知识

静默安装

静默安装是指apk无感安装,不需要用户确认。目前一般来说有两种方法可以实现:

  • 类似adb install指令
  • 使用PackageManager的installPackage接口,需要权限且是系统应用才行

第一种方法的示例代码为:

public static int installSilent(String filePath) {  
    File file = new File(filePath);  
    if (filePath == null || filePath.length() == 0 || file == null || file.length() <= 0 || !file.exists() || !file.isFile()) {  
        return 1;  
    }  
    String[] args = { "pm", "install", "-r", filePath };  
    ProcessBuilder processBuilder = new ProcessBuilder(args);  
    Process process = null;  
    BufferedReader successResult = null;  
    BufferedReader errorResult = null;  
    StringBuilder successMsg = new StringBuilder();  
    StringBuilder errorMsg = new StringBuilder();  
    int result;  
    try {  
        process = processBuilder.start();  
        successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));  
        errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));  
        String s;  
        while ((s = successResult.readLine()) != null) {  
            successMsg.append(s);  
        }  
        while ((s = errorResult.readLine()) != null) {  
            errorMsg.append(s);  
        }  
    } catch (IOException e) {  
        e.printStackTrace();  
    } catch (Exception e) {  
        e.printStackTrace();  
    } finally {  
        try {  
            if (successResult != null) {  
                successResult.close();  
            }  
            if (errorResult != null) {  
                errorResult.close();  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
        if (process != null) {  
            process.destroy();  
        }  
    }  
    if (successMsg.toString().contains("Success") || successMsg.toString().contains("success")) {  
        result = 0;  
    } else {  
        result = 2;  
    }  
    Log.d("test-test", "successMsg:" + successMsg + ", ErrorMsg:" + errorMsg);  
    return result;  
}

使用installPackage接口,是需要系统应用,且需要申明权限的

 <uses-permission android:name="android.permission.INSTALL_PACKAGES" />  

有一点需要注意,在安装中pms会检查另一种权限,类似于应用能否发通知,如果isUserRestricted返回为true,安装会失败,那么需要调用相关接口,重新设置下

if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
        try {
            if (observer != null) {
                observer.onPackageInstalled("", INSTALL_FAILED_USER_RESTRICTED, null, null);
            }
        } catch (RemoteException re) {
        }
        return;
    }

apk安装流程简析

PackageManager是一个抽象类,应用调用pm安装apk,这中间会发生跨进程调用,因为pms是运行在system进程中的。

为了更方便用户调用,于是Android封装了pm类供用户调用。在ContextImpl中,获取pm,实质上是获得了pm的实现类,ApplicationPackageManager。

查看pm的installPackage类

public abstract void installPackage(
        Uri packageURI, PackageInstallObserver observer,
        int flags, String installerPackageName);

查看pm的子类ApplicationPackageManager,发现最终将调用pms的installPackage方法

public void installPackageAsUser(String originPath, IPackageInstallObserver2 observer,
        int installFlags, String installerPackageName, VerificationParams verificationParams,
        String packageAbiOverride, int userId) {
    //权限检查,INSTALL_PACKAGES权限及其它权限检查,权限并不只有一种
    mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, null);
    final int callingUid = Binder.getCallingUid();
    enforceCrossUserPermission(callingUid, userId, true, true, "installPackageAsUser");
    if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
        try {
            if (observer != null) {
                observer.onPackageInstalled("", INSTALL_FAILED_USER_RESTRICTED, null, null);
            }
        } catch (RemoteException re) {
        }
        return;
    }
    //使用各参数,构建InstallParams,发送msg,交由handler处理
    final File originFile = new File(originPath);
    final OriginInfo origin = OriginInfo.fromUntrustedFile(originFile);
    final Message msg = mHandler.obtainMessage(INIT_COPY);
    msg.obj = new InstallParams(origin, observer, installFlags,
            installerPackageName, verificationParams, user, packageAbiOverride);
    mHandler.sendMessage(msg);
}

installPackageAsUser方法中主要完成两件事情,1是权限检查,2是构建InstallParams,然后发送INIT_COPY的msg。

此处比较有意思,因为pms是跨进程调用,所以installPackageAsUser方法运行在binder线程中,为什么还要将耗时的工作交给mHandler(mHandler在pms的构造方法中初始化,也是运行在一个单独的线程中,不是主线程)呢?

客户端调用binder服务端方法,客户端是会阻塞的,如果服务端的方法耗时较长,不利于客户端的流畅性。所以虽然服务端自己的主线程没问题,但客户端有问题,个人也经常这样考虑,使用handler处理,handler是异步,完全不会阻塞。不知道android的设计人员是不是也这样考虑的

void doHandleMessage(Message msg) {
    switch (msg.what) {
    case INIT_COPY: {
        HandlerParams params = (HandlerParams) msg.obj;
        int idx = mPendingInstalls.size();
        //如果没有绑定IMediaContainerService服务,则先绑定服务
        if (!mBound) {
            //绑定服务
            if (!connectToService()) {
                params.serviceError();
                return;
            } else {
                //绑定服务成功后,将params添加到mPendingInstalls队列当中
                mPendingInstalls.add(idx, params);
            }
        } else {
            mPendingInstalls.add(idx, params);
            if (idx == 0) {
                mHandler.sendEmptyMessage(MCS_BOUND);
            }
        }
        break;
    }
    }
}

在INIT_COPY的消息处理中,先查看当前有没有绑定IMediaContainerService服务,当服务绑定成功的时候

public void onServiceConnected(ComponentName name, IBinder service) {
        if (DEBUG_SD_INSTALL) Log.i(TAG, "onServiceConnected");
        IMediaContainerService imcs =
            IMediaContainerService.Stub.asInterface(service);
        mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs));
    }

mHandler将发送MCS_BOUND消息,同时也会将params添加到mPendingInstalls队列当中。

mPendingInstalls添加元素比较有意思,先查看mPendingInstalls的size,然后在size位置添加新元素,当元素使用完以后,则删除0位置上的元素,这就保证了先入先出。

void doHandleMessage(Message msg) {
    switch (msg.what) {
    case MCS_BOUND: {
        if (mContainerService == null) {
            //出錯
        } else if (mPendingInstalls.size() > 0) {
            HandlerParams params = mPendingInstalls.get(0);
            if (params != null) {
                if (params.startCopy()) {
                    // 删除已经完成的任务
                    if (mPendingInstalls.size() > 0) {
                        mPendingInstalls.remove(0);
                    }
                    if (mPendingInstalls.size() == 0) {
                        if (mBound) {
                            //如果任务队列中没有任务了,则发送MCS_UNBIND,解绑服务
                            removeMessages(MCS_UNBIND);
                            Message ubmsg = obtainMessage(MCS_UNBIND);
                            sendMessageDelayed(ubmsg, 10000);
                        }
                    } else {
                        //如果任务队列中还有新任务,则继续发送MCS_BOUND消息,循环执行新消息
                        mHandler.sendEmptyMessage(MCS_BOUND);
                    }
                }
            }
        }
        break;
    }
    }
}

MCS_BOUND消息的处理中,调用startCopy方法,完成apk的拷贝。

final boolean startCopy() {
    boolean res;
    try {
        if (++mRetries > MAX_RETRIES) {
            //如果重试超过三次,则直接放弃
            mHandler.sendEmptyMessage(MCS_GIVE_UP);
            handleServiceError();
            return false;
        } else {
            //真正开始复制的地方
            handleStartCopy();
            res = true;
        }
    } catch (RemoteException e) {
        mHandler.sendEmptyMessage(MCS_RECONNECT);
        res = false;
    }
    //复制完成后后续事宜
    handleReturnCode();
    return res;
}

继续查看handleStartCopy方法。

之前绑定的服务,在此处主要有两个功能,一是解析apk中的基本信息,比如包名、版本号、安装位置等

pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,
                    packageAbiOverride);

另外则是执行拷贝。当拷贝执行完以后,handleStartCopy方法也就告一段落了。我们继续看handleReturnCode方法

private void processPendingInstall(final InstallArgs args, final int currentStatus) {
    // Queue up an async operation since the package installation may take a little while.
    mHandler.post(new Runnable() {
        public void run() {
            mHandler.removeCallbacks(this);
             // Result object to be returned
            PackageInstalledInfo res = new PackageInstalledInfo();
            res.returnCode = currentStatus;
            res.uid = -1;
            res.pkg = null;
            res.removedInfo = new PackageRemovedInfo();
            if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
                //预安装
                args.doPreInstall(res.returnCode);
                synchronized (mInstallLock) {
                    //安装
                    installPackageLI(args, res);
                }
                //结束安装
                args.doPostInstall(res.returnCode, res.uid);
            }
            if (!doRestore) {
                //如果完成安装的msg,package add的广播将在此处发送
                Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);
                mHandler.sendMessage(msg);
            }
        }
    });
}

如上所示,最重要的4个步骤已经标明,预安装,安装应用以及完成安装,并发送package add等

其中doPreInstall和doPostInstall方法较简单,不再复述,主要查看installPackageLI方法。

installPackageLI方法非常长,它需要验证apk的签名文件,并且详细解析apk中的所有activity、service等信息并加以保存,方法非常非常的长

//收集签名并验证
try {
        pp.collectCertificates(pkg, parseFlags);
        pp.collectManifestDigest(pkg);
    } catch (PackageParserException e) {
        res.setError("Failed collect during installPackageLI", e);
        return;
    }

//详细解析apk
PackageParser.Package newPackage = scanPackageLI(pkg, parseFlags, scanFlags,
                System.currentTimeMillis(), user);

代码实在是太长了,读起来非常非常累,以后再详细解析

当handleReturnCode也完成后,mHandler将处理POST_INSTALL消息,完成安装,发送package add 广播

installd进程意义

这一小节将完全是个人的猜测,首先pms是运行在system的进程中的,而android中使用system的uid,并没有访问应用程序目录的权限(不能访问/data/data/包名
目录)。所以在pms之外还有一个进程存在,installd,它的代码位置是:

/frameworks/native/cmds/installd/installd.cpp

它也有install或者odex优化的方法,但在install方法中只看到创建data/data/包名 等目录,并没有其它操作。

这点需要后续去验证

installd与java端的installer类以socket通信。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容