为了方便安装Android软件,APK安装器这类软件在网上出现了,大家用的最多的可能就属 91手机助手自带的安装器了。今天我们使用 C++动手来编写一个这样的工具,在开始之前,我们有必要了解一下APK文件是怎样的方式安装到用户手机中的。细分起来,Android程序的安装有下面四种方式:
1.系统程序安装:开机时安装,这类安装没有安装界面
2.通过Market安装:直接通过Market进行网络安装,这类安装没有安装界面
3.ADB工具安装:使用ADB工具进行安装,这类安装没有安装界面
4.手机自带安装:通过SD卡里的APK文件安装,这类安装有安装界面这四种方法,仔细想想,我们能下手的就是通过 ADB工具来安装了,ADB(Android调试桥)是AndroidSDK中为开发人员提供的一款强大的程序调试测试工具,集成了安装,卸载,调试,设备管理等多个功能,详细的adb命令参数自己可以运行adb得到。现在我们只关心需要用到的功能。当 Android手机设备使用 USB调试模式连上计算机后,可以运行adb.exe devices来列出连接上的设备,如图1所示为开启一个Android模拟器后的,在设备已连接的情况下,可以直接运行 adb install APK file安装一个 APK程序或运行adb -s serial number install APK file来对指定的设备进行软件安装,后者是在连接多个设备的情况下才用的,我们这里为了简单就不考虑了,安装效果。在经过一小段时间的等待后,提示Success说明安装成功,不过在实际安装的过程中,有时候adb返回后没有Success提示但程序也安装成功了,这让人很郁闷,因为这样就无法捕获APK是否已经正确安装了,为了简化,我直接返回成功了。
APK程序的卸载也很简单,输入adb uninstall package就可以了,这里的package是软件的包名,每个APK程序安装后有个APK程序名,也有个包名,我们可以通过获取APK文件里面的信息来获取这个包名。卸载效果。
如果输入的package在手机中不存在的话,会返回如图4所示的结果。
这样就可以通过查看返回的数据里面有无Failure来判断APK是否成功卸载。到这里,我们可以完成手机是否已经正常连接、安装、卸载等功能了,我们继续看看如何获取 APK文件本身的信息,我们通常见到的APK安装器都可以显示APK文件的软件名、包名、文件大小、最低系统版本、权限等数据。这些数据的获取可以通过运行AAPT.exe来获取,AAPT是Android SDK中提供的另外一个工具,它提供了对APK进行信息列举、文件添加、文件删除及资源打包到APK等功能。我们要用到的是它的dump命令。
我们只需运行aapt d badging APK file就可以获取一个APK的详细信息。
这里的“package:name=”就是软件的包名,“versionName”就是软件的版本,“application: label”就是软件对外显示的名称,“icon=”就是软件显示的图标,“uses-permission:”就是软件用到的权限,这些权限有一部分是流氓及病毒软件需要用到的,在安装 APK之前,我们有必要查看它所需的权限来对它的危险度进行粗略的判断,“sdkVersion:”就是软件运行允许的Android最低版本,这里的数字7表示是Android2.1系统,不同版本的Android都有相应数字版本号,截止到最新的Android4.0版本它们的对应关系。
还有一种情况,当APK文件名无效或非法的APK文件的时候,aaptdbadgingAPKfile会返回错误信息。
这可能是 AAPT处理上的一个 BUG,我们将文件名改成普通英文名后即可正常返回 APK文件信息,所以,大家在安装APK程序的时候,请将APK文件名改成英文并且不要包含空格。本来AAPT与ADB这两个工具在Android源码中都可以找到,但是提取所需相关功能出来还绝非易事,研究了一下果断的放弃了,不过从技术研究的角度出发,我觉得大家还是可以深入研究的。整个APK安装器的思路就是通过CreateProcess调用AAPT与ADB进行相应的操作,而返回数据的获取我用到了管道,使用CreatePipe创建读句柄,在父进程中通过ReadFile来读取数据。
介绍到这里,我们需要的信息都已经获取了,下面是代码时间。APK文件的操作我封装进了CAPKHeaper类,类的声明如下:
class CAPKHelper { protected: mapstring,string m_mapPermission; string m_adbPath; string m_aaptPath; string m_apkName; string m_softName; string m_PkgName; string m_Version; string m_OSVersion; string m_FileSize; string m_Permission; string m_iconFile; public: CAPKHelper(const char *aaptPath,constchar *adbPath, const char*apkFile); ~CAPKHelper(void); protected: const string PrePare(APKFTyte flag); //准备命令字符串 const string DoCommand(const string cmd); //执行命令并返回结果 void InitPermissionMap(); public: void AdbPath(const char* val); void AaptPath(const char* val); const char* ApkName() const; void ApkName(const char * val); const char* AdbPath() const; const char* AaptPath() const; const char* Permission() const ; const char* AppName() const; const char* PkgName() const; const char* Version() const; const char* OSVersion() const; const char* FileSize() const; void GetInfo(); BOOL GetDevices(); BOOL Install(BOOL bSDCard); BOOL UnInstall(); void UnzipIcoFile(string outPath); }; 设备的检查使用GetDevices()方法,信息的获取使用GetInfo()方法,安装与卸载则分别是BOOL Install(BOOL bSDCard)与BOOL UnInstall(),看看GetDevices()的代码如下: BOOL CAPKHelper::GetDevices() { string str; str = DoCommand(PrePare(APKF_DEVICES)); string::size_type posbegin; posbegin = str.find(devices); if (posbegin == string::npos) return false; else { posbegin = posbegin + devices.size(); string strDevice = ; strDevice.assign(str, posbegin, str.size()-posbegin); posbegin = strDevice.find(device); if(posbegin == string::npos) return false; } return true; } 代码首先通过PrePare(APKF_DEVICES)来获取需要执行的命令字符串,不同的命令类型用了一个枚举量来表示: typedef enum tagApkFType{//flags for PrePare APKF_INFO, APKF_DEVICES, APKF_INSTALL, APKF_INSTALLSDCARD, APKF_UNINSTALL } APKFTyte; 获取命令后通过DoCommand()来执行并返回执行结果,然后对结果进行处理。接下来看看PrePare的代码: const string CAPKHelper::PrePare( APKFTyte flag ) { string cmd = ; switch (flag) { case APKF_INFO: if(m_apkName.empty() || m_aaptPath.empty()) return ; cmd = m_aaptPath + d badging \ + m_apkName + ; break; case APKF_DEVICES: if(m_adbPath.empty()) return ; cmd = m_adbPath + devices ; ; break; case APKF_INSTALL: if(m_apkName.empty() || m_adbPath.empty()) return ; cmd = m_adbPath + install \ + m_apkName + ; break; case APKF_INSTALLSDCARD: if(m_apkName.empty() || m_adbPath.empty()) return ; cmd = m_adbPath + install -s \ + m_apkName + ; break; case APKF_UNINSTALL: if(m_appName.empty() || m_adbPath.empty()) return ; cmd = m_adbPath + uninstall + m_PkgName; break; default: return ; break; } return cmd; } 这个方法很简单,就不多讲了,类的执行核心在DoCommand中,代码如下: const string CAPKHelper::DoCommand( const string cmd ) { STARTUPINFO si; SECURITY_ATTRIBUTES sa; PROCESS_INFORMATION pi; string str=; char ReadBuf[1024] = {0}; DWORD ReadNum; HANDLE hRead; //管道读句柄 HANDLE hWrite; //管道写句柄 sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.bInheritHandle = true; sa.lpSecurityDescriptor = NULL; BOOL bRet = CreatePipe(hRead, hWrite, sa, 0); //创建匿名管道 if(!bRet) return ; GetStartupInfo(si); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; si.hStdOutput = hWrite; si.hStdError = hWrite; si.hStdInput = hRead; bRet = CreateProcess(NULL, (char *)cmd.c_str(), NULL, NULL, true, CREATE_NEW_CONSOLE, NULL, NULL, si, pi); if (!bRet) { CloseHandle(hRead); CloseHandle(hWrite); return ; } CloseHandle(hWrite); WaitForSingleObject(pi.hProcess, 3000); //等待执行返回 while(ReadFile(hRead, ReadBuf, sizeof(ReadBuf)-1, ReadNum, NULL)) { ReadBuf[ReadNum] = \0; str = str + ReadBuf; } str = utf2ansi(str.c_str(), str.size()); CloseHandle(hRead); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return str; }
这段代码是典型的 WIN32代码,首先通过 CreatePipe()创建管道,设置子进程的读写句柄,然后创建子进程并获取返回信息,在这里需要注意的是AAPT返回的是UTF-8编码的字符串,如果APK软件名为中文的话我们直接获取的话在程序中就会显示为乱码,所以需要加上这行代码:str = utf2ansi(str.c_str(), str.size()),它的作用就是将UTF8格式编码的字符串转换为ANSI编码:
std::string utf2ansi(LPCSTR pszSrc, int nLen) { int nSize = MultiByteToWideChar(CP_UTF8, 0, (LPCSTR)pszSrc, nLen, 0, 0); if(nSize = 0) return NULL; WCHAR *pwsz = new WCHAR[nSize+1]; if( NULL == pwsz) return NULL; MultiByteToWideChar(CP_UTF8, 0,(LPCSTR)pszSrc, nLen, pwsz, nSize); pwsz[nSize] = 0; char *psz = new char[nSize+1]; WideCharToMultiByte(CP_ACP, 0, pwsz, nSize, psz, nSize, NULL, NULL); string str = psz; delete pwsz; delete psz; return str; }
GetInfo()、Install()、Uninstall()对返回的数据进行分析并给出最终的执行结果,Install()与 Uninstall()代码很简单,就不列举了,大家可以看已查看源码,具体的说说GetInfo(),它的处理还是相对较难的,GetInfo()通过DoCommand()接收到类似图6的数据并进行提取,然后设置到类的相应成员当中,为了使字符串操作简化,我使用了基于STL模板类中的string类,这个类很强大,提供了字符串除正则表达式外的一切操作,这使得GetInfo()的代码量更少更美观,截取一段处理代码如下:
void CAPKHelper::GetInfo() { string str; str = DoCommand(PrePare(APKF_INFO)); string::size_type posbegin; string::size_type posend; posbegin = str.find(errorstr); if (posbegin != string::npos) { //APK文件是否有效 m_softName = ; m_PkgName = ; m_iconFile = ; m_Version = ; m_OSVersion = ; m_FileSize = ; m_Permission = ; return; } posbegin = str.find(softName); if (posbegin == string::npos) m_softName = ; else { posbegin = posbegin + softName.size(); posend = str.find(\, posbegin); m_softName.assign(str, posbegin, posend-posbegin); //获取APK的软件对外显示名称 } posbegin = str.find(pkgName); if (posbegin == string::npos) m_PkgName = ; else { posbegin = posbegin + pkgName.size(); posend = str.find(\, posbegin); m_PkgName.assign(str, posbegin, posend-posbegin); //获取 APK的软件包名称 } ...... ...... HANDLE hApk = CreateFile(m_apkName.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if(hApk == INVALID_HANDLE_VALUE) m_FileSize = ; else { DWORD dwSize = GetFileSize(hApk, NULL); DOUBLE fSize = 0; string strend = ; if (dwSize = 1024*1024) { fSize = dwSize * 1.0 / 1024 / 1024; strend = MB; } else if (dwSize = 1024) { fSize = dwSize * 1.0 / 1024; strend = KB; } else { fSize = dwSize; strend = 字节; } char buf[16] = {0}; m_FileSize = _gcvt(fSize, 3, buf); m_FileSize += strend; CloseHandle(hApk); } posbegin = str.find(iconfile); if (posbegin == string::npos) m_OSVersion = ; else { } posbegin = posbegin + iconfile.size(); posend = str.find(\, posbegin); m_iconFile.assign(str, posbegin, posend-posbegin); }
使用string类的find方法对特征字符串进行查找,特征字符串在上面讲解ADB与AAPT时有用到,它们分别是:
const string errorstr = ERROR: dump failed; const string appName = application: label=; const string pkgName = package: name=; const string version = versionName=; const string osversion = sdkVersion:; const string permission = uses-permission:; const string iconfile = icon=; const string devices = List of devices attached;
如果找到相应的特征字符串就提取相应的值并设置相应的成员变量,反之则设为空。权限字符串的处理是比较麻烦的,查找permission会返回一系列以 uses-permission:打头的字符串,而我们程序需要显示的是权限的描述,为此,我将这些权限与之对应的描述创建到了一个STL的Map对象中,mapstring,stringm_mapPermission,而初始化的操作封装到了InitPermissionMap()中,代码如下:
void CAPKHelper::InitPermissionMap() { m_mapPermission.clear(); m_mapPermission.insert(pairstring,string(android.permission.ACCESS_COARSE_LOCATION,允许获取粗略的位置信息)); m_mapPermission.insert(pairstring,string(android.permission.ACCESS_FINE_LOCATION,允许访问GPS)); ...... ...... m_mapPermission.insert(pairstring,string(android.permission.ACCESS_MOCK_LOCATION,获取模拟定位信息)); m_mapPermission.insert(pairstring,string(android.permission.CHANGE_COMPONENT_ENABLED_STATE,改变组件启用状态)); }
这样访问权限描述就很简单了,代码如下:
string strTmp = ; string strInfo = ; posbegin = 0; while((posbegin = str.find(permission, posbegin)) != string::npos) { posbegin = posbegin + permission.size(); posend = str.find(\, posbegin); strTmp.assign(str, posbegin, posend-posbegin); //获取APk使用到的权限 strInfo = m_mapPermission[strTmp]; if(!strInfo.empty()) m_Permission = m_Permission + strInfo + \n; posbegin = posend; //下一个 }
这个CAPKHeaper类就讲到这里,大家可以在代码中查看类关系图更详细了解它的流程,我们接下来看看主程序的逻辑,先看看程序设计的界面,如图8所示:
图8
程序的编写基于MFC的对话框,我们看看安装按钮的响应:
voidCAPKInstallerDlg::OnBnClickedInstall() { if(!m_helper-GetDevices()) { m_syslnkShow.SetWindowText(手机未连接或手机驱动没有安装好,ahref=\http://*******\); return; } if(m_apkPath.empty()) OpenAPK(); if(m_apkPath.empty())return; m_threadType=THREADF_INSTALL; unsignedintdwThreadID=0; HANDLEh=(HANDLE)_beginthreadex(NULL,0,www.weixianmanbu.com ThreadProc,(LPVOID)this,0,dwThreadID); ; }
线程的创建使用了WIN32的_beginthreadex(),所有对APK的操作都封装进这个ThreadProc()线程中,代码如下:
unsigned__stdcallCAPKInstallerDlg::ThreadProc(LPVOIDlpParam) { EnterCriticalSection(g_cs); CAPKInstallerDlg*pWnd=(CAPKInstallerDlg*)lpParam; switch(pWnd-m_threadType) { caseTHREADF_DEVICES: pWnd-m_syslnkShow.SetWindowText(正在检查已连接的手机); if(pWnd-m_helper-GetDevices()) { pWnd-m_syslnkShow.SetWindowText(手机连接良好); } else { pWnd-m_syslnkShow.SetWindowText(手机未连接或手机驱动没有安装好,a href=\http://*******\点击此处/a下载驱动); } break; caseTHREADF_INSTALL: pWnd-m_btnUninstall.EnableWindow(FALSE); pWnd-m_syslnkShow.SetWindowText(正在安装软件...); pWnd-m_helper-Install(pWnd-m_cbPath.GetCurSel()); pWnd-m_syslnkShow.SetWindowText(软件安装完成); pWnd-m_btnUninstall.EnableWindow(TRUE); break; caseTHREADF_UNINSTALL: pWnd-m_btnInstall.EnableWindow(FALSE); pWnd-m_syslnkShow.SetWindowText(正在卸载软件...); if(pWnd-m_helper-UnInstall()) { pWnd-m_syslnkShow.SetWindowText(软件卸载完成); } else { pWnd-m_syslnkShow.SetWindowText(软件卸载失败); } pWnd-m_btnInstall.EnableWindow(TRUE); break; } LeaveCriticalSection(g_cs); return1; }
各种操作信息的显示我使用了syslink控件,使用它的一个好处就是这个控件支持如html的链接一样的显示效果,它们的鼠标点击处理代码统一如下:
voidCAPKInstallerDlg::OnNMClickLinkurl(NMHDR*pNMHDR,LRESULT*pResult) { PNMLINKpNMLink=(PNMLINK)pNMHDR; wstringwstr=pNMLink-item.szUrl; stringstr=wstring2string(wstr); ::ShellExecute(m_hWnd,open,str.c_str(),NULL,NULL,SW_SHOWNORMAL); *pResult=0; }
注意这行:wstringwstr=pNMLink-item.szUrl,syslink点击消息返回的链接地址为WCHAR*类型,这使用我们使用前需要先转换一下:
stringwstring2string(constwstringws) { _bstr_tt=ws.c_str(); char*pchar=(char*)t; stringresult=pchar; returnresult; }
说说APK文件图标的获取,在上面图6显示的信息中,就能够知道APK图标的文件名与位置,而这个文件存放在APK这个ZIP包中,AAPT不提供ZIP解压的功能,需要自己来提取APK中的文件,这里为了代码轻便使用了XUNZIP类,获取图标并解压的工作放到了voidUnzipIcoFile(stringoutPath)方法中:
voidCAPKHelper::UnzipIcoFile(stringoutPath) { HZIPhZip=OpenZip((void*)(char*)m_apkName.c_str(),0,ZIP_FILENAME); intindex=-1; ZIPENTRYze; ZRESULTbRet=FindZipItem(hZip,m_iconFile.c_str(),TRUE,index,ze); if(bRet==ZR_OK) { UnzipItem(hZip,index,(void*)outPath.c_str(),0,ZIP_FILENAME); } CloseZip(hZip); }
在主程序中处理如下:
charszBuffer[MAX_PATH]={0}; GetTempPath(MAX_PATH,szBuffer); m_outpngPath=szBuffer; m_outpngPath+=m_helper-PkgName(); m_outpngPath+=m_helper-Version(); m_outpngPath+=.png; //APK文件图标释放路径 m_helper-UnzipIcoFile(m_outpngPath.c_str()); ShowPNG();//显示图标
最后调用ShowPNG()来显示图标,一般APK中的图标为PNG,在VC中显示PNG使用GDI+再方便不过了,代码如下:
voidCAPKInstallerDlg::ShowPNG() { if(!m_outpngPath.empty()) { wstringstr=string2wstring(m_outpngPath); Imageimage(str.c_str()); RECTrc; m_stcPic.GetWindowRect(rc); m_pGraphics-DrawImage(image,0,0,rc.right-rc.left,rc.bottom-rc.top); } }
但是直接显示PNG后,程序如果被刷新图片就不再显示了,因此我们需要在WM_PAINT消息中做进一步的处理:
voidCAPKInstallerDlg::ShowPNG() { if(!m_outpngPath.empty()) { wstringstr=string2wstring(m_outpngPath); Imageimage(str.c_str()); RECTrc; m_stcPic.GetWindowRect(rc); m_pGraphics-DrawImage(image,0,0,rc.right-rc.left,rc.bottom-rc.top); } }
在调用ShowPNG()之前,CDialog::UpdateWindow()这行代码一定要先加上,否则图标照样不会显示,好了,整体上程序代码很简单,一目了然,到这里我们的程序就算是完成了,看看运行效果如下图:
如果文中有任何语言描述或代码上的错误,欢迎大家批评指正,同时也欢迎大家与我一起交流Android技术。