編輯:關於Android編程
在本篇博客中,我們分析一下Android中的APK是如何安裝的,以及PKMS在這個過程中進行了哪些工作。
APK的安裝方式有很多,我們先來看看如何用adb命令進行安裝。
我們從adb install開始分析,該命令有多個參數,這裡僅考慮最基本的adb install xxxx.apk。
一、adb命令
看看system/core/adb/commandline.cpp中的adb_commandline函數:
int adb_commandline(int argc, const char **argv) { ........... else if (!strcmp(argv[0], "install")) { if (argc < 2) return usage(); FeatureSet features; std::string error; if (!adb_get_feature_set(&features, &error)) { fprintf(stderr, "error: %s\n", error.c_str()); return 1; } if (CanUseFeature(features, kFeatureCmd)) { //支持FeatureCmd時調用install_app return install_app(transport_type, serial, argc, argv); } //否則,利用install_app_legacy return install_app_legacy(transport_type, serial, argc, argv); } ........... }
1、install_app_legacy
我看先看看傳統的install_app_legacy:
static int install_app_legacy(TransportType transport, const char* serial, int argc, const char** argv) { //待安裝的APK目前還在源機器上,現在需要把APK的文件復制到手機裡 //如果安裝在手機內部存儲,那麼目的地址為DATA_DEST //如果安裝在SD卡上,則目的地址為SD_DEST static const char *const DATA_DEST = "/data/local/tmp/%s"; static const char *const SD_DEST = "/sdcard/tmp/%s"; ......... //默認安裝到手機內部 const char* where = DATA_DEST; for (i = 1; i < argc; i++) { //攜帶參數-s時,才安裝到SD卡 if (!strcmp(argv[i], "-s")) { where = SD_DEST; } } //解析參數,判斷adb命令中是否攜帶了有效的apk文件名 ........... //取出apk名 std::vectorapk_file = {argv[last_apk]}; //構造apk目的地址 std::string apk_dest = android::base::StringPrintf( where, adb_basename(argv[last_apk]).c_str()); //do_sync_push將此APK文件傳輸到手機的目標路徑,失敗的話將跳轉到clenaup_apk if (!do_sync_push(apk_file, apk_dest.c_str())) goto cleanup_apk; //執行pm_command result = pm_command(transport, serial, argc, argv); cleanup_apk: //刪除剛才傳輸的文件 //PKMS在安裝過程中會將該APK復制一份到/data/app目錄下,所有data/local/tmp目錄下對應的文件可以刪除 delete_file(transport, serial, apk_dest); return result; }
從代碼來看,傳統的安裝方式就是將源機器中的APK文件拷貝到目的手機的tmp目錄下,然後調用pm_command進行處理。
2、install_app
我們再看看支持FeatureCmd的機器,如何安裝APK:
static int install_app(TransportType transport, const char* serial, int argc, const char** argv) { //利用參數創建出本地文件的名稱 const char* file = argv[argc - 1]; //解析參數,判斷adb命令中是否攜帶了有效的apk文件名 ......... //adb_open中將創建出這個file對應的文件 int localFd = adb_open(file, O_RDONLY); ............ std::string cmd = "exec:cmd package"; //添加cmd參數 ............ //連接源端,獲取源APK文件的描述符 int remoteFd = adb_connect(cmd, &error); ............ //將remoteFd中的數據寫入到localFd copy_to_file(localFd, remoteFd); //得到結果 read_status_line(remoteFd, buf, sizeof(buf)); adb_close(localFd); adb_close(remoteFd); .......... return 0; }
從代碼來看install_app就是將源機器的文件復制到了目的機器中,並沒有進行額外的操作。猜想可能是支持特殊FeatureCmd的機器,PKMS能夠監聽到這個拷貝,然後觸發後續的掃描工作。這個過程沒有研究過對應代碼,暫時不做深入分析。
對於傳統的安裝方式,我們需要繼續往下看看pm_command。
二、pm_command
我們先看看pm_command函數:
static int pm_command(TransportType transport, const char* serial, int argc, const char** argv) { std::string cmd = "pm"; //構造pm cmd while (argc-- > 0) { cmd += " " + escape_arg(*argv++); } //發送shell命令給adbd return send_shell_command(transport, serial, cmd, false); }
我們跟進下send_shell_command:
// Connects to the device "shell" service with |command| and prints the // resulting output. static int send_shell_command(TransportType transport_type, const char* serial, const std::string& command, bool disable_shell_protocol, std::string* output=nullptr, std::string* err=nullptr) { ........... while (true) { bool attempt_connection = true; // Use shell protocol if it's supported and the caller doesn't explicitly disable it. if (!disable_shell_protocol) { ....... if (adb_get_feature_set(&features, &error)) { //如果定義了feature,則替換shell protocol use_shell_protocol = CanUseFeature(features, kFeatureShell2); } else { // Device was unreachable. attempt_connection = false; } } if (attempt_connection) { std::string error; //此時command中攜帶的就是以pm開頭的命令 std::string service_string = ShellServiceString(use_shell_protocol, "", command); //向shell服務發送命令 fd = adb_connect(service_string, &error); if (fd >= 0) { break; } } ............ } //讀取返回結果 int exit_code = read_and_dump(fd, use_shell_protocol, output, err); if (adb_close(fd) < 0) { .......... } return int exit_code; }
從上面的代碼來看,pm_command就是向shell服務發送pm命令。
pm是一個可執行腳本,我們在終端上調用adb shell,然後執行pm,可以得到以下結果:
root:/ # pm usage: pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER] pm list permission-groups pm list permissions [-g] [-f] [-d] [-u] [GROUP] pm list instrumentation [-f] [TARGET-PACKAGE] ..........
pm腳本定義在frameworks/base/cmds/pm中:
base=/system export CLASSPATH=$base/framework/pm.jar exec app_process $base/bin com.android.commands.pm.Pm "$@"
在編譯system.img時,會根據Android.mk將該腳本復制到system/bin目錄下。
從腳本的內容來看,當調用pm時,將向app_process目錄的main函數傳入Pm對應的參數:
我們看看對應的定義於app_main.cpp的main函數(前面的博客分析過,這個其實也是zygote啟動的函數):
//app_process的main函數 int main(int argc, char* const argv[]) { ........ //解析參數 while (i < argc) { const char* arg = argv[i++]; if (strcmp(arg, "--zygote") == 0) { zygote = true; niceName = ZYGOTE_NICE_NAME; } else if (strcmp(arg, "--start-system-server") == 0) { startSystemServer = true; } else if (strcmp(arg, "--application") == 0) { application = true; } else if (strncmp(arg, "--nice-name=", 12) == 0) { niceName.setTo(arg + 12); } else if (strncmp(arg, "--", 2) != 0) { //此時我們有參數,進入該分支設置className className.setTo(arg); break; } else { --i; break; } } ........... if (zygote) { runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (className) { //此時不再是啟動zygote,而是啟動className對應的類 runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } else { ......... } ........ }
我們跟進AndroidRuntime.cpp的start函數:
void AndroidRuntime::start(const char* className, const Vector& options, bool zygote) { .......... jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); } else { //反射調用main函數,從native層進入java世界 env->CallStaticVoidMethod(startClass, startMeth, strArray); } ......... }
於是流程會進入到RuntimeInit的main函數:
public static final void main(String[] argv) { ........ //進行一些常規的初始化工作 commonInit(); /* * Now that we're running in interpreted code, call back into native code * to run the system. */ nativeFinishInit(); ......... }
native函數定義在framework/base/core/jni/AndroidRuntime.cpp中,對應的函數為:
static void com_android_internal_os_RuntimeInit_nativeFinishInit(JNIEnv* env, jobject clazz) { //gCurRuntime保存AndroidRuntime,實際上是AndroidRuntime的子類 gCurRuntime->onStarted(); }
App_main.cpp中定義的AppRuntime繼承AndroidRuntime,實現了onStarted函數:
virtual void onStarted() { //binder通信相關的 spproc = ProcessState::self(); ALOGV("App process: starting thread pool.\n"); proc->startThreadPool(); AndroidRuntime* ar = AndroidRuntime::getRuntime(); //調用AndroidRuntime.cpp的callMain函數,參數與Pm.java相關 ar->callMain(mClassName, mClass, mArgs); IPCThreadState::self()->stopProcess(); }
status_t AndroidRuntime::callMain(const String8& className, jclass clazz, const Vector& args) { .......... env = getJNIEnv(); .......... methodId = env->GetStaticMethodID(clazz, "main", "([Ljava/lang/String;)V"); .......... const size_t numArgs = args.size(); stringClass = env->FindClass("java/lang/String"); strArray = env->NewObjectArray(numArgs, stringClass, NULL); for (size_t i = 0; i < numArgs; i++) { jstring argStr = env->NewStringUTF(args[i].string()); env->SetObjectArrayElement(strArray, i, argStr); } ........... //最終調用了Pm.java的main函數 env->CallStaticVoidMethod(clazz, methodId, strArray); return NO_ERROR; }
這裡自己初次看時,認為這裡沒有fork新的進程,那麼APK安裝運行在zygote進程中。
實際上這是一個錯誤的理解,說明自己的理解還不到位。
init創建zygote進程時,是fork出一個子進程,然後才調用app_main中的函數,此時整個zygote嚴格來講只是一個native進程;當app_main函數最終通過AndroidRuntime等反射調用zygoteInit.java的main函數後,才演變成了Java層的zygote進程。
這裡的情況是類似的,adb進程發送消息給Shell服務,Shell服務執行Pm腳本,由於exec函數並未創建出新的進程,因此調用app_main後整個代碼仍然是運行在Shell服務對應的native進程中,同樣通過反射後演變為Java層中的進程。
這裡自己花了很多的筆墨來分析如何從執行腳本文件,到啟動Java進程。
主要是弄懂這個機制後,我們實際上完全可以學習pm的寫法,依葫蘆畫瓢寫一個腳本文件,然後定義對應的Java文件。
通過腳本命令,來讓Java層的進程提供服務。
最後,我們通過一個圖來總結一下這個過程:
三、Pm中的流程
現在我們進入了Pm.java的main函數:
public static void main(String[] args) { int exitCode = 1; try { //別被寫法欺騙了,Pm並沒有繼承Runnable exitCode = new Pm().run(args); } catch (Exception e) { ....... } System.exit(exitCode); } //根據參數進行對應的操作,現在我們僅關注APK安裝 public int run(String[] args) throws RemoteException { ........... //利用Binder通信,得到PKMS服務端代理 mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); //保存參數 mArgs = args; String op = args[0]; mNextArg = 1; ............ //返回PKMS中保存的PackageInstallerService mInstaller = mPm.getPackageInstaller(); ........ if ("install".equals(op)) { //安裝APK將調用runInstall return runInstall(); } ....... }
我們跟進runInstall函數:
private int runInstall() throws RemoteException { //根據參數創建InstallParams,其中包含了SessionParams,標志為MODE_FULL_INSTALL final InstallParams params = makeInstallParams(); //1 創建Session final int sessionId = doCreateSession(params.sessionParams, params.installerPackageName, params.userId); try { //inPath對應於安裝的APK文件 final String inPath = nextArg(); ....... //2 wirite session if (doWriteSession(sessionId, inPath, params.sessionParams.sizeBytes, "base.apk", false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { return 1; } //3 commit session if (doCommitSession(sessionId, false /*logSuccess*/) != PackageInstaller.STATUS_SUCCESS) { return 1; } System.out.println("Success"); return 0; } finally { ........ } }
從上面的代碼來看,runInstall主要進行了三件事,即創建session、對session進行寫操作,最後提交session。
接下來,我們來看看每一步究竟在干些什麼:
1、 create session
private int doCreateSession(SessionParams params, String installerPackageName, int userId) throws RemoteException { //通過ActivityManagerService得到"runInstallCreate"(作為Context對應的字符串)對應的uid userId = translateUserId(userId, "runInstallCreate"); if (userId == UserHandle.USER_ALL) { userId = UserHandle.USER_SYSTEM; params.installFlags |= PackageManager.INSTALL_ALL_USERS; } //通過PackageInstallerService創建session final int sessionId = mInstaller.createSession(params, installerPackageName, userId); return sessionId; }
跟進一下PackageInstallerService的createSession函數:
@Override public int createSession(SessionParams params, String installerPackageName, int userId) { try { return createSessionInternal(params, installerPackageName, userId); } catch (IOException e) { throw ExceptionUtils.wrap(e); } } private int createSessionInternal(SessionParams params, String installerPackageName, int userId) throws IOException { //安裝權限檢查 ....... //修改SessionParams的installFlags if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) { params.installFlags |= PackageManager.INSTALL_FROM_ADB; } else { ......... } .......... // Defensively resize giant app icons //調整app圖標大小,這裡應該是不同安裝方式共用的代碼 //通過adb安裝apk時,應該還沒有解析到app圖標 if (params.appIcon != null) { ........ } //根據SessionParams的installFlags進行一些操作 .......... } else { // For now, installs to adopted media are treated as internal from // an install flag point-of-view. //adb安裝應該進入這個分支(不添加參數指定安裝在sd card時),為SessionParams設置InstallInternal Flag,後文會用到 params.setInstallFlagsInternal(); ........... } final int sessionId; final PackageInstallerSession session; synchronized (mSessions) { // Sanity check that installer isn't going crazy //確保同一個uid沒有提交過多的Session,MAX_ACTIVE_SESSIONS為1024 final int activeCount = getSessionCount(mSessions, callingUid); if (activeCount >= MAX_ACTIVE_SESSIONS) { throw new IllegalStateException( "Too many active sessions for UID " + callingUid); } //同樣確保同一個uid沒有提交過多的Session,MAX_HISTORICAL_SESSIONS為1048576 final int historicalCount = getSessionCount(mHistoricalSessions, callingUid); if (historicalCount >= MAX_HISTORICAL_SESSIONS) { throw new IllegalStateException( "Too many historical sessions for UID " + callingUid); } ........ //sessionId是個隨機值 sessionId = allocateSessionIdLocked(); // We're staging to exactly one location File stageDir = null; String stageCid = null; //根據installFlags,決定安裝目錄,前文已經提到,過默認將安裝到internal目錄下 if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) { final boolean isEphemeral = (params.installFlags & PackageManager.INSTALL_EPHEMERAL) != 0; //此處將會在臨時性的data目錄下創建出file,應該是作為copy的目的地址 stageDir = buildStageDir(params.volumeUuid, sessionId, isEphemeral); } else { stageCid = buildExternalStageCid(sessionId); } session = new PackageInstallerSession(mInternalCallback, mContext, mPm, mInstallThread.getLooper(), sessionId, userId, installerPackageName, callingUid, params, createdMillis, stageDir, stageCid, false, false); mSessions.put(sessionId, session); mSessions.put(sessionId, session); } //進行回調 mCallbacks.notifySessionCreated(session.sessionId, session.userId); //在mSessionsFile中進行記錄 writeSessionsAsync(); return sessionId; }
從代碼來看,上述代碼的目的就是為APK安裝做好准備工作,例如權限檢查、目的臨時文件的創建等, 最終創建出PackageInstallerSession對象。PackageInstallerSession可以看做是”安裝APK”這個請求的封裝,其中包含了處理這個請求需要的一些信息。
這種設計方式,大致可以按照命令模式來理解。
實際上PackageInstallerSession不僅是分裝請求的對象,其自身還是個服務端:
public class PackageInstallerSession extends IPackageInstallerSession.Stub
前言開發做得久了,總免不了會遇到各種坑。而在Android開發的路上,『軟鍵盤擋住了輸入框』這個坑,可謂是一個曠日持久的巨坑——來來來,我們慢慢看。入門篇最基本的情況,如
今天要做一個任務,要求圖片做按鈕開關,點擊出發相應事件。點擊打開,圖片左邊顯示幾行字體,這幾行字體是延時顯示的。下面將主要代碼附上。以下是main.xml
先上一下可以實現的效果圖 要實現的效果有幾方面 1、列不固定:可以根據數據源的不同生成不同的列數 2、表格內容可以根據數據源的定義合並列 3、要填寫的單元格可