如何利用安卓版apktoolapktool 反编译失败安卓软件,让它改变颜色,,透明,,,,教程详细点

Android APK程序反编译步骤详细图解_百度文库
两大类热门资源免费畅读
续费一年阅读会员,立省24元!
Android APK程序反编译步骤详细图解
阅读已结束,下载文档到电脑
想免费下载更多文档?
定制HR最喜欢的简历
下载文档到电脑,方便使用
还剩1页未读,继续阅读
定制HR最喜欢的简历
你可能喜欢{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"contributes":[{"sourceColumn":{"lastUpdated":,"description":"腾讯工程师,定期Android / iOS 技术分享","permission":"COLUMN_PUBLIC","memberId":,"contributePermission":"COLUMN_PUBLIC","translatedCommentPermission":"all","canManage":true,"intro":"腾讯工程师,定期Android / iOS 技术…","urlToken":"bugly","id":11296,"imagePath":"e1ff32e9e1114d0ceba728a65623eed8.jpeg","slug":"bugly","applyReason":"","name":"遇见","title":"遇见","url":"/bugly","commentPermission":"COLUMN_ALL_CAN_COMMENT","canPost":true,"created":,"state":"COLUMN_NORMAL","followers":4972,"avatar":{"id":"e1ff32e9e1114d0ceba728a65623eed8","template":"/{id}_{size}.jpeg"},"activateAuthorRequested":false,"following":false,"imageUrl":"/e1ff32e9e1114d0ceba728a65623eed8_l.jpeg","articlesCount":60},"state":"accepted","targetPost":{"titleImage":"/f6c94e7fbb376beb2d3dda_r.jpg","lastUpdated":,"imagePath":"f6c94e7fbb376beb2d3dda.jpg","permission":"ARTICLE_PUBLIC","topics":[],"summary":"很多人写文章,喜欢把什么行业现状啊,研究现状啊什么的写了一大通,感觉好像在写毕业论文似的,我这不废话,先直接上几个图,感受一下。 第一张图是在把代码注入到地图里面,启动首页的时候弹出个浮窗,下载网络的图片,苍老师你们不会不认识吧?第二张图…","copyPermission":"ARTICLE_COPYABLE","translatedCommentPermission":"all","likes":0,"origAuthorId":0,"publishedTime":"T14:11:01+08:00","sourceUrl":"","urlToken":,"id":546319,"withContent":false,"slug":,"bigTitleImage":false,"title":"【Bugly干货分享】手把手教你逆向分析 Android 程序","url":"/p/","commentPermission":"ARTICLE_ALL_CAN_COMMENT","snapshotUrl":"","created":,"comments":0,"columnId":11296,"content":"","parentId":0,"state":"ARTICLE_PUBLISHED","imageUrl":"/f6c94e7fbb376beb2d3dda_r.jpg","author":{"bio":"移动端用户反馈神器","isFollowing":false,"hash":"133d72fca32c969f1f84d88abd96752d","uid":24,"isOrg":false,"slug":"bugly","isFollowed":false,"description":"腾讯Bugly,面向移动开发者提供最专业的Crash监控、崩溃分析等质量跟踪服务,帮助您修复用户的每一次Crash!","name":"干掉crash","profileUrl":"/people/bugly","avatar":{"id":"70a08c690f3ea399d656","template":"/{id}_{size}.png"},"isOrgWhiteList":false},"memberId":7732856,"excerptTitle":"","voteType":"ARTICLE_VOTE_CLEAR"},"id":362904}],"title":"【Bugly干货分享】手把手教你逆向分析 Android 程序","author":"bugly","content":"很多人写文章,喜欢把什么行业现状啊,研究现状啊什么的写了一大通,感觉好像在写毕业论文似的,我这不废话,先直接上几个图,感受一下。第一张图是在把代码注入到地图里面,启动首页的时候弹出个浮窗,下载网络的图片,苍老师你们不会不认识吧?第二张图是微信运动步数作弊,6不6?ok,那我们从头说起1.反编译Android 的反编译,相信大家都应该有所了解,apktool、JEB 等工具我们先看一下 Apk 文件的结构吧,如下图:1.META-INF:签名文件(这个是如何生成的后面会提到)2.res:资源文件,里面的 xml 格式文件在编译过程中由文本格式转化为二进制的 AXML 文件格式3.AndroidManifest.xml:android 配置文件,编译过程依然被转换为 AXML 格式4.classes.dex:java 代码编译后产生的类似字节码的文件(dalvik 字节码)5.resources.arsc:具有 id 值资源的索引表(asserts 文件夹中的资源不会生成索引)6.其他文件:可由开发者自己添加,诸如 assets 等,或者 lib(native so 代码)等目录(Android 编译打包过程分析参看:)apk的核心逻辑主要在 classes.dex 中,破解和二次打包也基本上对这个文件做手脚,所以对这个文件的保护也尤为重要。上图为一般 Apk 的破解过程(windows 画图工具画的比较搓)我们首先用 apktool 工具反编译:java -jar apktool.jar d -f xxx.apk outDir(PS:outDir 不写会在当前目录输出)反编译后的目录结构如下:这里,res 里的 xml 和 manifset.xml 都已经是解出后的 xml 了,不是 axml 格式了,res 目录里的 values 目录下的 public.xml 可以看到资源对应的 id如果命令 java -jar apktool.jar d -f再加入 -r 代表资源文件不反编译,上图的目录中将依然有resources.arsc,xml 都是 axml 格式的,也找不到 public.xml其实我们主要关注的是 smali 这个目录,里面是按照 android 程序编写的时候 java 文件的目录格式生成的,但是里面的文件并不是 java 格式的,而是 smali 格式的,类似 MainActivity.smali。那么什么是 smali 文件呢?1.Smali 是 Android 的 Dalvik 虚拟机所使用的一种 dex 格式的中间语言2.可以理解为,C 语言和汇编语言的编译与反编译,把 smali 理解为一种汇编语言我们可以打开一个 smali 文件看看,我们可以使用 notepad++ 打开,然后定一下 smali 语法的高亮显示将下面内容保存到 C:\\Users\\用户名\\AppData\\Roaming\\Notepad++下,文件名为 userDefineLang.xml&NotepadPlus&\n
&UserLang name=\"smali\" ext=\"smali\"&\n
&Settings&\n
&Global caseIgnored=\"no\" /&\n
&/Settings&\n
&KeywordLists&\n
&Keywords name=\"Delimiters\"&&/Keywords&\n
&Keywords name=\"Folder+\"&&/Keywords&\n
&Keywords name=\"Folder-\"&&/Keywords&\n
&Keywords name=\"Operators\"&' ! \" ( ) , ; : @ [ ] { }&/Keywords&\n
&Keywords name=\"Comment\"&0#&/Keywords&\n
&Keywords name=\"Words1\"&move move/from16 move/16 move-wide move-wide/from16 move-wide/16 move-object move-object/from16 move-object/16 move-result move-result-wide move-result-object move-exception return-void return return-wide return-object const/4 const/16 const const/high16 const-wide/16 const-wide/32 const-wide const-wide/high16 const-string const-string/jumbo const-class monitor-enter monitor-exit check-cast instance-of array-length new-instance new-array filled-new-array filled-new-array/range fill-array-data throw goto goto/16 goto/32 packed-switch sparse-switch cmpl-float cmpg-float cmpl-double cmpg-double cmp-long if-eq if-ne if-lt if-ge if-gt if-le if-eqz if-nez if-ltz if-gez if-gtz if-lez aget aget-wide aget-object
aget-boolean aget-byte aget-char aget-short aget-short aput aput-wide aput-object aput-boolean aput-byte aput-char aput-short iget iget-wide iget-object iget-boolean iget-char iget-short iput iput-wide iput-object iput-boolean iput-byte iput-char iput-short sget sget-wide sgetobject sget-boolean sget-byte sget-char sget-short sput sput-wide sput-object sput-boolean sput-byte sput-char sput-short invoke-virtual invoke-super invoke-direct invoke-static invoke-interface invoke-virtual/range invoke-super/range invoke-direct/range invoke-static/range invoke-interface/range neg-int not-int neg-long neg-float neg-double int-tolong int-tofloat int-to-double long-to-int long-to-float long-to-double float-to-int float-to-long double-to-double double-to-int double-to-long double-to-float int-to-byte int-to-char int-to-short add-int sub-int mul-int div-int rem-int and-int or-int xor-int shl-int shr-int ushr-int add-long sub-long mul-long div-long rem-long and-long or-long xor-long shl-long shr-long ushr-long add-float sub-float mul-float div-float rem-float add-double sub-double mul-double div-double rem-double add-int/2addr sub-int/2addr mul-int/2addr div-int/2addr rem-int/2addr and-int/2addr or-int/2addr xor-int/2addr shl-int/2addr shr-int/2addr usnhr-int/2addr add-long/2addr sub-long/2addr mul-long/2addr div-long/2addr rem-long/2addr and-long/2addr or-long/2addr xor-long/2addr shl-long/2addr shr-long/2addr ushr-long/2addr add-float/2addr sub-float/2addr mul-float/2addr div-float/2addr rem-float/2addr add-double/2addr mul-double/2addr div-double/2addr rem-double/2addr add-int/lit16 rsub-int mul-int/lit16 div-int/lit16 and-int.lit16 or-int/lit16 xor-int/lit16 and-int/lit8 mul-int/lit8 div-int/lit8&/Keywords&\n
&Keywords name=\"Words2\"&.method .annotation .end
.line .prologue .implements .super .class .source
.locals .parameter .field .local .restart&/Keywords&\n
&Keywords name=\"Words3\"&public annotation method protected static final field private synthetic local&/Keywords&\n
&Keywords name=\"Words4\"&Z V I F&/Keywords&\n
&/KeywordLists&\n
&Styles&\n
&WordsStyle name=\"DEFAULT\" styleID=\"11\" fgColor=\"000000\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&WordsStyle name=\"FOLDEROPEN\" styleID=\"12\" fgColor=\"FF0000\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&WordsStyle name=\"FOLDERCLOSE\" styleID=\"13\" fgColor=\"FF0000\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&WordsStyle name=\"KEYWORD1\" styleID=\"5\" fgColor=\"FF8040\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"1\" fontSize=\"10\" /&\n
&WordsStyle name=\"KEYWORD2\" styleID=\"6\" fgColor=\"91A62D\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"2\" fontSize=\"10\" /&\n
&WordsStyle name=\"KEYWORD3\" styleID=\"7\" fgColor=\"004080\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"0\" fontSize=\"10\" /&\n
&WordsStyle name=\"KEYWORD4\" styleID=\"8\" fgColor=\"FF0000\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"0\" fontSize=\"10\" /&\n
&WordsStyle name=\"COMMENT\" styleID=\"1\" fgColor=\"FF8080\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"2\" fontSize=\"10\" /&\n
&WordsStyle name=\"COMMENT LINE\" styleID=\"2\" fgColor=\"008000\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"2\" fontSize=\"10\" /&\n
&WordsStyle name=\"NUMBER\" styleID=\"4\" fgColor=\"D9006C\" bgColor=\"FFFFFF\" fontName=\"Consolas\" fontStyle=\"0\" fontSize=\"10\" /&\n
&WordsStyle name=\"OPERATOR\" styleID=\"10\" fgColor=\"008040\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&WordsStyle name=\"DELIMINER1\" styleID=\"14\" fgColor=\"AF2BFF\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&WordsStyle name=\"DELIMINER2\" styleID=\"15\" fgColor=\"AF2BFF\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&WordsStyle name=\"DELIMINER3\" styleID=\"16\" fgColor=\"000000\" bgColor=\"FFFFFF\" fontName=\"\" fontStyle=\"0\" /&\n
&/Styles&\n
&/UserLang&\n&/NotepadPlus&\n可以参看
操作打开 MainActivity.smali 文件,头三行代码大致如下:.class public Lcom/example/hacktest/MainA\n.super Landroid/app/A\n.source \"MainActivity.java\"\nsmali 语法这里就不介绍了,自己查资料就好 smali 文件语法参考 这里举个例子,我们写个程序,一个 edittext 一个 button,当 edit 里面输入正确的文字的时候,点击 button 才会弹出正确的 toast。我们看一下,反编译后的关键代码可以看到这是一个参数为 string,返回值为 boolean 名叫 check 的函数,当输入为“11”的时候才返回 true。我们可以把第一个红框位置的 if-eqz 改成 if-nez,这样你输入除了11的任何字符都会返回 true。或者把第二个红框位置的 0x1,改成 0x0,(0代表 true),这样这个函数不管输入什么都返回 true。OK,这样我们就改完了,我们重新编译:java -jar apktool.jar b -f outDir xxx.apk(PS:xxx.apk 可以不写,会在 outDir 里生成 dist 目录,编译好的 Apk 在这里面)签名:可以网上下载工具 autoSign,使用方法略。安装 Apk 后验证,通过。但是事情并不总是如我们所愿,有些 Apk 会做一些盗版检测机制,就是为了防止二次重打包以手机暴风影音为例,当你按照上述步骤反编译,重新编译,签名之后,进入 APP 会出现这个页面,无法正常使用因为你并没有这个 APP 的正版签名文件(关于签名相关的东西,在后面我再仔细讲)那么这个原理是什么呢,我们大胆猜测一下,无非就是和上一个例子类似的 check 函数,两个值的对比,那么这个值一定是签名。我们先通过方法拿到正版手机暴风影音的签名 md5,然后在反编译后的代码中搜索一下这个值。然而并没有搜到,再换个思路,我们搜索获取签名的这个函数,从而定位关键代码,获取应用签名的 java 代码类似是:PackageInfo pi = context.getPackageManager.().getPackageInfo(packname,packageManager.GET_SIGNATURES);\n对应的smali代码类似是:\nLandroid/content/pm/PackageI-&signatures:[Landroid/content/pm/Signature\n我们再搜用这段代码搜索,在 StormUtils2.smali 里面找到了,发现在函数 getSignInfo 里面,继续跟踪到 checkPiracy 函数。看到这个函数发现就和上例中的 check 函数类似了,改一下返回值为 true 就好了。我们再仔细看看这个函数,发现关键的签名 md5 值被拆开存放了,所以我们才没有搜到,这也是防范搜索的一个举措吧(虽然我觉得并没什么用)const-string/jumbo v3, \"dbbf60f096b326003\"\nconst-string/jumbo v0, \"c388a350d1578d5\"\n好的,修改后,我们再重新编译、签名,验证通过。(PS:关于签名检测的除了 java 层的,可能还有再 so 里面校验的和服务器验证的方式,在 so 里的用 IDA 打开 so 跟踪修改,服务器验证的抓包查看,再模拟发包重放攻击就好了,这里就不具体介绍了)2. Android 的签名保护机制到底是什么?android 系统禁止更新安装签名不一致的 apk,如果我们修改了 apk 又用别的签名文件签名,肯定是不一致的。我们从签名工具 autoSign 分析,看一下 sign.bat 文件内容:@ECHO OFFEcho Auto-sign Created By Dave Da illest 1Echo Update.zip is now being signed and will be renamed to update_signed.zipjava -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apkEcho Signing CompletePauseEXIT看一下 java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk 这行的意义:以testkey.x509.pem 这个公钥文件和 testkey.pk8 这个私钥文件对 update.apk 进行签名,签名后保存为 update_signed.apk我们可以看到签名前和签名后比较,签名后的文件中多了一个文件夹“META-INF”,里面有三个文件 MANIFEST.MF 、 CERT.SF 、 CERT.RSA我们通过 jd-gui 工具打开 signapk.jar,找到 main 函数,通过这个函数跟踪代码1.addDigestsToManifest 这个函数,遍历 apk 中所有文件,对非文件夹非签名文件的文件逐个生成 SHA1 数字签名信息,再 base64 编码然后再写入 MANIFEST.MF 文件中,生成文件如下:Manifest-Version: 1.0Created-By: 1.0 (Android)Name: res/drawable-xhdpi/ic_launcher.pngSHA1-Digest: AfPh3OJoypH966MludSW6f1RHg4=Name: AndroidManifest.xmlSHA1-Digest: NaPhUBH5WO7uGk/CfRu/SHsCvW0=Name: res/drawable-mdpi/ic_launcher.pngSHA1-Digest: RRxOSvpmhVfCwiprVV/wZlaqQpw=Name: res/drawable-hdpi/ic_launcher.pngSHA1-Digest: Nq8q3HeTluE5JNCBpVvNy3BXtJI=Name: res/layout/activity_main.xmlSHA1-Digest: kxwMyILwF2K+n9ziNhcQqcCGWIU=Name: resources.arscSHA1-Digest: q7Ystu6WoSWih53RGKXtE3LeTdc=Name: classes.dexSHA1-Digest: Ao1WOs5PXMxsWTDsjSijS2tfnHo=Name: res/drawable-xxhdpi/ic_launcher.pngSHA1-Digest: GVIfdEOBv4gEny2T1jDhGGsZOBo=SHA1 生成的摘要信息,如果你修改了某个文件,apk 安装校验时,取到的该文件的摘要与 MANIFEST.MF 中对应的摘要不同,则安装不成功2.接下来对之前生成的 manifest 使用 SHA1withRSA 算法, 用私钥签名,writeSignatureFile 这个函数,最后生成 CERT.SF 文件,如下:Signature-Version: 1.0Created-By: 1.0 (Android)SHA1-Digest-Manifest: pNZ9UXN9GMqTgqAwKD6uEN6aD34=Name: res/drawable-xhdpi/ic_launcher.pngSHA1-Digest: cIga++hy5wqjHl9IHSfbg8tqCug=Name: AndroidManifest.xmlSHA1-Digest: oRzzLkwuvxC78suvJcAEvTqcjSA=Name: res/drawable-mdpi/ic_launcher.pngSHA1-Digest: VY7kOF8E3rn8EUTvQC/DcBEN6kQ=Name: res/drawable-hdpi/ic_launcher.pngSHA1-Digest: stS7pUucSY0GgAVoESyO3Y7SanU=Name: res/layout/activity_main.xmlSHA1-Digest: Yr3img6SqiKB+1kwcg/Fga2fwcc=Name: resources.arscSHA1-Digest: j1g8I4fI9dM9hAFKEtS9dHsqo5E=Name: classes.dexSHA1-Digest: Sci9MmGXNGnZ1d04rCrEEV7MWn4=Name: res/drawable-xxhdpi/ic_launcher.pngSHA1-Digest: KKqaLh/DVvFp+v1KoaDw7xETvrI=用私钥通过 RSA 算法对 manifest 里的摘要信息进行加密,安装的时候只能通过公钥解密,解密之后才能获得正确的摘要,再对比3.最后就是如何生成 CERT.RSA,打开这个文件看到的是乱码,说明整个文件都被编码加密了,而且这个文件和公钥有关从源码中看出他是通过 PKCS7 将整个文件加密了总结:1.签名只是对完整性和签名发布机构的校验机制 2.不能阻止 apk 被修改,只是签名无法保持一致 3.不同私钥对应着不同的公钥,实质上不同的公钥就代表了不同的签名3. Android 系统如何获取签名我们从获取下面一段代码开始分析:packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES );\nSignature[] signs = packageInfo.\nmd5 = getMD5Str(signs[ 0].toByteArray());\ncontext.getPackageManager()其实拿到的是ApplicationPackageManager\n看一下 context 的实现类 contextImpl 里的 getPackageManager()@Override\npublic PackageManager getPackageManager() {\n
if (mPackageManager != null) {\n
return mPackageM\n
IPackageManager pm = ActivityThread.getPackageManager ();\n
if (pm != null) {\n
// Doesn't matter if we make more than one instance.\n
return (mPackageManager = new ApplicationPackageManager(this, pm));\n
}\n\\n}\n可以看到 ActivityThread 中方法 getPackageManager 获取
IPackageManager继续看 ActivityThread 的代码:public static IPackageManager getPackageManager() {\nif (sPackageManager != null) {\n
//Slog.v(\"PackageManager\", \"returning cur default = \" + sPackageManager);\n
return sPackageM\n}\nIBinder b = ServiceManager.getService(\"package\");\n//Slog.v(\"PackageManager\", \"default service binder = \" + b);\nsPackageManager = IPackageManager.Stub.asInterface(b);\n//Slog.v(\"PackageManager\", \"default service = \" + sPackageManager);\nreturn sPackageM\n}\n看源码知道是通过 ServiceManager.getService(“package”);获取 PackageManagerService 并得到 IBinder对象,然后通过
asInterface 函数取得接口类 IPackageManager 实例然后作为参数构造 ApplicationPackageManager再看 ApplicationPackageManager 这个类里的方法:@Override\npublic PackageInfo getPackageInfo(String packageName, int flags)\n
throws NameNotFoundException {\n
PackageInfo pi = mPM.getPackageInfo(packageName, flags, mContext.getUserId());\n
if (pi != null) {\n \n
} catch (RemoteException e) {\n
throw new RuntimeException( \"Package manager has died\" , e);\n
throw new NameNotFoundException(packageName);\n}\n这里的mPM就是上面构造函数传进来的 IPackageManager,就可以调用 PackageManagerService 的方法了下图是 PackagerManager 静态类结构图:ok,看完上图的结构图,继续跟代码 ,看到这里是 mPM 继续调用的getPackageInfo(代理模式),通过进程通信到 PackageManagerService 中执行响应操作@Override\npublic PackageInfo getPackageInfo(String packageName, int flags, int userId) {\n
if (!sUserManager.exists(userId))\n
enforceCrossUserPermission(Binder.getCallingUid (), userId, false, \"get package info\");\n
// reader\n
synchronized (mPackages) {\n
PackageParser.Package p = mPackages.get(packageName);\n
if (DEBUG_PACKAGE_INFO)\n
Log.v (TAG, \"getPackageInfo \" + packageName + \": \" + p);\n
if (p != null) {\n
return generatePackageInfo(p, flags, userId);\n
if((flags & PackageManager. GET_UNINSTALLED_PACKAGES ) != 0) {\n
return generatePackageInfoFromSettingsLPw(packageName, flags, userId);\n
}\\n}\n这里 mPackages 是 hashMap,其调用put方法的时机是在 scanPackageLI 方法中,而 scanPackageLI 的调用地方是在程序安装和替换函数中,还有就是 scanDirLi 中,代码略。scanDirLi 是用来扫描一些系统目录的的,在 PackageManagerService 的构造函数中调用的File dataDir = Environment. getDataDirectory();\nmAppDataDir = new File(dataDir, \"data\");\nmAppInstallDir = new File(dataDir, \"app\");\nmAppLibInstallDir = new File(dataDir, \"app-lib\" );\nmAsecInternalPath = new File(dataDir, \"app-asec\" ).getPath();\nmUserAppDataDir = new File(dataDir, \"user\");\nmDrmAppPrivateInstallDir = new File(dataDir, \"app-private\" );\n上面是扫描的位置↑↑↑↑↑↑↑PackageManagerService 处理各种应用的安装、卸载、管理等工作,开机时由 systemServer 启动此服务。就是说之前安装过的应用或者系统应用信息都会在开机扫描过程中存到 mPackages 这个 hashMap 中。开机后用户的安装操作也会同样存到这个 hashMap 里面。继续看 getPackageInfo,调用的 generatePackageInfo , 里面调用的是 PackageParser 中的 generatePackageInfo,继续跟这个函数的代码比较长,只贴出部分关键代码:if ((flags&PackageManager. GET_SIGNATURES ) != 0) {\n
int N = (p.mSignatures != null) ? p.mSignatures.length : 0;\n
if (N & 0) {\n
pi.signatures = new Signature[N];\n
System.arraycopy (p.mSignatures, 0, pi. signatures, 0 , N);\n
}\\n这段代码之前主要是做一些复制的操作,就是 new 一个 PackageInfo,然后把 PackageParser.Package 对象中的一些内容复制到这个 PackageInfo 中从这段代码可以看出来,最终得到的 signatures 信息就是中 p(PackageParser.Package )中的成员 mSignatures 中得来的。好了,现在就是看这个PackageParser.Package是从哪来的了,通过跟踪代码,installPackageLI 和 scanPackageLI 中的 final PackageParser.Package pkg = pp.parsePackag (tmpPackageFile, null, mMetrics, parseFlags);这行代码只是生成了 pkg,并没有赋值里面的 mSignatures,继续跟踪,找到函数 collectCertificatesLI,找到 pp.collectCertificates(pkg , parseFlags)
PS:最终又进行了一系列的跟代码,找到了 JarVerifier.java 这个类的 readCertificates 这个就是用来读取.RSA 文件的,最终我们看到的里面的代码是通过 loadCertificates 取得的 certs 赋值给了 pkg.mSignatures至此,获取签名的所有逻辑就算是简单的过一遍了,以下是简略流程图:(发现 ppt 画图比 Windows 画图工具好用多了,哈哈)在跟代码的过程中也找到了签名对比的函数 compareSignatures ,有空自己看看就好了。OK,绕了这么久我们终于找到源头了,获取签名就是在 META-INF 中寻找,并解析。试想一下,如果我们修改了这个函数,让他解析原来正版的 META-INF 中的 CERT.RSA 文件,这样就可以伪造为真正的签名了。那么我们就想到了 HOOK,(很多人都是从看雪论坛上找到的一篇文章看到的 ) hook 的原来简单来说就是,找到原函数和新函数的指针位置,然后兑换内容,将新函数替代原函数。关于 hook 呢,网上也有很多框架可以使用,比如:1.Cydia substrate : 2.Xposed : 网上也有很多教程可以看看乌云(wooyun)上有一篇很有意思的教程,就是利用 hook 进行微信运动作弊,原帖地址:下图就是我用了上面的方法产生的效果,还差点被微信部门的人请去喝茶。这里用的是 Xposed 框架,原理就是 hook 了手机的计步传感器的队列函数,然后把步数的返回值每步乘1000返回,前提是,你的手机硬件本身有计步传感器功能,这里微信运动里面列出了支持的手机列表:。好像最高就是98800了,可能是微信做了步数限制吧我这里用的是小米4联通版,发现虽然是1000基数的加,但是好像隔了很久才变化,估计又是 MIUI 做了一些省电策略,传感器的采集做了对齐吧?Xposed 框架,很多玩机爱好者,会拿它修改一些主题,字体之类的,或者系统界面,定制自己想要的系统插件等等。然而,也有缺点,需要手机root,而且这个框架,还有可能让手机变砖,还有的系统可能对这个框架支持的不好,或者不支持。XDA 论坛里面也有很多大神把 Xposed 对某些机型做了适配,大神一般都是说,如果手机变砖他们不负责,哈哈。正式由于这些框架的诸多不便,root 等等的问题,于是就有了一些非 root 的 hook 的黑科技,比如阿里巴巴的开源框架 Dexposed()其也是根据 Xposed 框架修改而来的,不过看 github 上他们也好久没更新了。也有像其他个人写的和这种比较类似的框架,这里就不介绍了。但是这类框架的缺点就是,只能在该进程下hook,不能全局 hook,即只对这个进程的应用起作用,不能对另一个应用起作用,优点是可以 hook 自定义函数也能hook 系统函数,并且不用 root 和重启。阿里用这个框架来打在线热补丁。那对于 APP 内部签名校验的就不用再搜相应的代码了,直接 hook 就一步到位了,android.app.ApplicationPackageManager 这个类的 getPackageInfo 这个方法直接把正确的签名返回就好了,接下来我们就需要把 hook 的代码注入到某个 APP 里就好了。4. 关于如何注入?开篇的时候有个图片就是我在腾讯地图里面注入了一个苍老师的图片其实就是,自己写了个 imageloader,用来下载网络图片,再写个 activity 或者 dialog 来承载这个 imageview,然后编译,再反编译,取出相应的smali等文件,比如贴到已经反编译好的腾讯地图的里面,把开启这个苍老师图片下载的启动代码放到合适位置,最后再把腾讯地图重新打包签名,就ok 了。hook 代码也是同理注入,验证一下,成功(我这块写的比较粗略,代码比较多,只说思路了)那么这种代码注入和 hook 相结合的方式能干什么呢,我们也不妨搞出点事情来。同样我们还是进行微信运动作弊的事情,其实很多运动类的软件都可以把自己的数据同步到微信运动里,比如小米手环,乐动力,悦动圈等等。那我们就先拿其中一个开刀吧:经过一系列的跟踪代码定位,最终定位到了这个类 cn.ledongli.ldl.cppwrapper.DailyStats 里的 f 方法(f 是因为代码混淆了)然后我们注入并 hook 方法,让它返回66666,ok,我们看到了如下效果:然后我们在应用里面登陆微信账号,和对接到微信运动的功能,发现不好用,是因为,微信里面做了对应用的签名校验,应用的签名已经变了所以我们只能破解微信了(闷声作大死),同样注入 hook 代码,让微信获取应用的签名的时候取得正确签名,关键代码:
if( packageName.equals( \"cn.ledongli.ldl\")){\n
if ( result instanceof PackageInfo) {\n
PackageInfo info = (PackageInfo)\n
info. signatures[0] = new Signature( myHexLedongli);\n
param.setResult( info);\n
}\n再把这个盗版的微信重新打包签名,重新进行应用的同步数据操作,再进微信运动看看,是不是已经66666了。至此作弊完成。♂♂♂♂♂♂♂♂♂♂我是画风不同的分割线♂♂♂♂♂♂♂♂♂♂♂♂说了这么多破解的,也该聊聊防破解的了google 最早给的就是代码混淆的方案,其实一般的混淆只是降低了代码的可读性,让你对反编译出来的函数命名等不知道什么意思,不过解读出来只是时间问题。后来还有资源混淆的,但是意义不大。后来有了核心代码用 C 实现,写成 SO,加花指令的办法,这个办法确实会阻止一大部分人的继续破解,但是对于经常做逆向的工程师来说也不是什么难题。其实做这么多大多数软件的初衷就是不想软件被盗版,然后被注入乱七八糟的广告,或者被盗取信息等,后来就有了盗版检测机制。比如:JAVA 层的签名校验,NDK 层校验,分段存放签名 Hash 串,服务器校验等等,但是这些方法我都在上面说了破解方法现在国内的一般应用市场都有对 APP 签名的检测,在你下载的时候会告诉你这个 APP 是不是盗版的,从而让用户区分出来。但是应用市场自己本身又被盗版了怎么办呢?再后来,就有了像360加固保和腾讯的乐固等产品,so 做了加密,真正的 dex 也藏起来了,不过个人觉得,就算真正的 dex 也需要变成 odex 了,root 的手机取到 odex,再转回 dex,就能拿到真正的 dex(虽然我没试过,但是我觉得可能是一个思路),所以这个方法就更难破解了虽然加固产品很厉害,但是也会有他的缺陷,Android 系统不断的更新升级,也许就换了某些模式等等,比如 ART 刚出来的时候,加固保加固后的 Apk,在 ART 模式运行下就会 Crash。这些加固产品要不断的适配各种型号的手机,CPU 类型,运行模式等等,所以很多 APP 为了考虑兼容性,他们也不会轻易去加固自己的产品。♂♂♂♂♂♂♂♂♂♂我是画风不同的分割线♂♂♂♂♂♂♂♂♂♂♂♂关于逆向破解 Android 应用,我觉得耐心很重要吧,代码跟来跟去确实很枯燥,总结几点小技巧吧1.信息反馈:通过界面的一些弹出信息,界面特点寻找突破点2.特征函数:比如搜 Toast,Log,getSignature 等3.代码注入:把 toast 或者 log 函数注入到程序中,跟踪位置4.打印堆栈:插入 new Exception(“定位”).printStackTrace();5.网络抓包:通过抓包得到的关键字段,在代码中定位写在后面:这篇文章整理了有一段时间了,觉得还是应该写出来,也不是什么高深的技术文章,就是个人总结的一点心得而已。关于破解应用很多人可能会去破解别人的应用注入广告来获取利益,也有可能盗取别人的信息。不过我们作为有节操的开发工程师,应该本着瑞雪的精神看待技术,学习技术,而不是乱♂搞。但是我们也应该知道,我们的应用有可能会被别人怎么搞……最后推荐一本资料书,大家可以有空看看。如果你觉得内容意犹未尽,如果你想了解更多相关信息,请扫描以下二维码,关注我们的公众账号,可以获取更多技术类干货,还有精彩活动与你分享~是一款专为移动开发者打造的工具,帮助开发者快速,便捷的定位线上的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 根据根因合并分类,每日日报会列出影响用户数最多的,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!","updated":"T06:11:01.000Z","canComment":false,"commentPermission":"anyone","commentCount":3,"collapsedCount":0,"likeCount":9,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","titleImage":"/f6c94e7fbb376beb2d3dda_r.jpg","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"Android"},{"url":"/topic/","id":"","name":"反编译"}],"adminClosedComment":false,"titleImageSize":{"width":900,"height":500},"href":"/api/posts/","excerptTitle":"","column":{"slug":"bugly","name":"遇见"},"tipjarState":"inactivated","annotationAction":[],"sourceUrl":"","pageCommentsCount":3,"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T14:11:01+08:00","url":"/p/","lastestLikers":[{"bio":null,"isFollowing":false,"hash":"5ba8bb9cd3e61399abdfb","uid":349060,"isOrg":false,"slug":"zhu-zhu-64-15","isFollowed":false,"description":"","name":"朱宁","profileUrl":"/people/zhu-zhu-64-15","avatar":{"id":"93c7c34ed2411ced97283cc6eda63784","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"","isFollowing":false,"hash":"a270f909b80a43d26e89","uid":44,"isOrg":false,"slug":"yu-cong-87","isFollowed":false,"description":"","name":"茅屋","profileUrl":"/people/yu-cong-87","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":null,"isFollowing":false,"hash":"d4f140e0c8a57aef7268e","uid":847300,"isOrg":false,"slug":"gu-shen-wei-65","isFollowed":false,"description":"","name":"羲和","profileUrl":"/people/gu-shen-wei-65","avatar":{"id":"fbd2ba3fda","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"是的,什么也没有","isFollowing":false,"hash":"3ac9cbd9b614f412dc78e","uid":84,"isOrg":false,"slug":"xie-hu-46-15","isFollowed":false,"description":"我们历经了苦难,所以才能更好的面对生活","name":"车某人","profileUrl":"/people/xie-hu-46-15","avatar":{"id":"26523acabc317b29edf02e32d825ed9e","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"多读书总是好的。","isFollowing":false,"hash":"d763f443f57e0f74cdfaf6","uid":08,"isOrg":false,"slug":"cangyeone","isFollowed":false,"description":"个人网站","name":"沧夜","profileUrl":"/people/cangyeone","avatar":{"id":"b7a98d0bc67","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false}],"summary":"很多人写文章,喜欢把什么行业现状啊,研究现状啊什么的写了一大通,感觉好像在写毕业论文似的,我这不废话,先直接上几个图,感受一下。 第一张图是在把代码注入到地图里面,启动首页的时候弹出个浮窗,下载网络的图片,苍老师你们不会不认识吧?第二张图…","reviewingCommentsCount":0,"meta":{"previous":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"性能测试"},{"url":"/topic/","id":"","name":"性能优化"},{"url":"/topic/","id":"","name":"Android"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"移动端用户反馈神器","isFollowing":false,"hash":"133d72fca32c969f1f84d88abd96752d","uid":24,"isOrg":false,"slug":"bugly","isFollowed":false,"description":"腾讯Bugly,面向移动开发者提供最专业的Crash监控、崩溃分析等质量跟踪服务,帮助您修复用户的每一次Crash!","name":"干掉crash","profileUrl":"/people/bugly","avatar":{"id":"70a08c690f3ea399d656","template":"/{id}_{size}.png"},"isOrgWhiteList":false},"content":" 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处。前言:那些年我们用过的显示性能指标注:Google 在自己文章中用了 Display Performance 来描述我们常说的流畅度,为了显得有文化,本文主要用“显示性能”一词来代指“流畅度”(虽然两者在概念上有细微差别)。从 Android 诞生的那一刻起,流畅度就为众人所关注。一时之间,似乎所有人都在讨论 Android 和 iOS 谁的流畅度更好。但是,毫不夸张的说,流畅度绝对是 Android 众多性能维度中最为奇葩的一个。因为,为了刻画这一性能维度,业界设计了各式各样的指标来对其进行衡量。可以说弄清了这些指标我们就明白了什么是流畅度,可是这似乎并不太容易。笔者简单搜集了一些业界中提及的显示性能指标,大家可以来品评一下:指标名称:FPS相关资料:指标名称:Aggregate frame stats(N 多个指标)相关资料:指标名称:Jankiness count、Max accumulated frames、Frame rate相关资料:指标名称:SM、Skipped frames相关资料:面对如此之多的显示性能指标,想必大家也会跟笔者一样,心中难免疑惑丛生。其实,我们只需要依次弄清楚以下三个哲学问题,所有的问题也许就会迎刃而解:你是谁——这些指标具体反映了什么问题你从哪儿来——这些指标数值是怎么得到的你要到哪儿去——这些指标如何落地来指导优化因此,本文将尝试依次从上诉三个问题来逐步分析和探讨各个显示性能指标。Step 1:你是谁——这些指标具体反映了什么问题总所周知,脱离了具体的应用背景,所有的指标都是没有意义的。所以,为了彻底弄清楚各个显示性能指标的具体身份,我们势必得从 Android 的图像渲染流程说起。具体展开之前,首先需要说明的是,为了降低复杂程度和本章篇幅,在这个环节之中,我们只讨论图像渲染流程中的各个具体环节所对应的指标有哪些。而指标的具体定义,由第二章《你从哪儿来——这些指标数值是怎么得到的》进行讨论。Android 图像渲染流程下图是笔者结合各类资料(主要是是源码及官方文档),在根据自己的理解梳理出的几种常见场景下的图像渲染流程:PS 1:笔者个人技术水平有限,若存在理解有误的地方还望指正。PS 2:本文主要讨论的 Android 源码为 Android 6.0备注:基于 OpenGL 的应用可以使用 Choreographer 中的 VSYNC 信号来进行图像渲染工作的安排。上面这幅图涉及的概念较多,要完全吃透估计得费不少时间。不过好在我们只是想弄明白显示性能各种指标的含义,所以我们只需要理清下面两大关系即可:SurfaceFlinger、HWComposer与Surface的关系Surface:可以理解为Android系统中的一个基本显示单元。只要使用Android任意一种API绘图,绘制的结果都将反映在Surface上。SurfaceFlinger:服务运行在System进程中,用来统一管理系统的帧缓冲区设备,其主要作用是将系统中的大部分Surface进行合成。SurfaceFlinger主要使用GPU进行Surface的合成,合成的结果将形成一个FrameBuffer。HWComposer:即Hardware Composer HAL,其作用是将SurfaceFlinger通过GPU合成的结果与其他Surface一起最终形成BufferQueue中的一个Buffer。此外,HWComposer可以协助SurfaceFlinger进行Surface的合成,但是否进行协助是由HWComposer决定的。值得注意的是,有的Surface不由WindowManager管理,将直接作为HWComposer的输入之一与SurfaceFlinger的输出做最后的合成。Choreographer、SurfaceFlinger、HWComposer与VSYNC的关系VSYNC:Vertical Synchronization的缩写,它的作用是使GPU的渲染频率与显示器的刷新频率(一般为固定值)同步从而避免出现画面撕裂的现象。HWComposer:VSYNC信号主要由HWComposer通过硬件触发。Choreographer:当收到VSYNC信号时,Choreographer将按优先级高低依次去调用使用者通过postCallback提前设置的回调函数,它们分别是:优先级最高的CALLBACK_INPUT、优先级次高的CALLBACK_ANIMATION以及优先级最低的CALLBACK_TRAVERSAL。SurfaceFlinger:Surface的合成操作也时基于VSYNC信号进行的。简单来说,Android 图像渲染流程主要由以下特征:我们可以简单把 Android 图像渲染架构分为应用(Surface)、系统(SurfaceFlinger)、硬件(Screen)三个层级,其中绘制在应用层,合成及提交上屏在系统层,显示在硬件层;无论应用(Surface)、系统(SurfaceFlinger)、硬件(Screen)都是当且仅当绘制内容发生改变,才会对绘制内容进行处理;系统中的 SurfaceFlinger 以及绝大部分 Surface 都是按照 VSYNC 信号的节奏来安排自己的任务;目前,绝大部分 Surface 都属于 Hardware Rendering。各个指标在 Android 图像渲染流程所代表的意义大致梳理了 Android 的图像渲染流程之后,我们需要做的一件事情,就是看看上面提到的指标,都对应了渲染流程的哪些阶段,这样对于我们了解各个指标所反映的具体物理意义及其优势劣势都有极大帮助。再次强调,在这个环节之中,我们的讨论仅限于只讨论指标所对应的渲染流程的具体阶段,各指标的具体定义由第二章具体展开。系统层级(SurfaceFlinger)的显示性能指标基础数据:SurfaceFlinger 合成次数指标意义:系统合成帧率:FPS特别说明:SurfaceFlinger 仅在显示区域内的 Surface 有提交内容更新时才会进行合成(上屏),因此,系统合成帧率低并不一定意味着图像显示性能差,有可能是因为当前并没有任何的内容更新所导致。若显示区域内的某个待测 Surface 持续进行更新时, SurfaceFlinger的合成(上屏)的频率可以在某种程度上反映该 Surface 的显示性能,但从理论上分析该指标并不一定准确。这是因为,若显示区域内尚存在其他 Surface,它们也会影响 SurfaceFlinger 的合成(上屏)的行为,从而干扰结果。若某个 Surface 的合成不在 SurfaceFlinger 中进行(如 Camera Preview),则该 Surface 的显示性能无法用这类指标进行衡量。应用层级(Surface)的显示性能指标基础数据:绘制过程中每一帧的关键时间点(如开始绘制时间、结束绘制时间等)指标意义:应用绘制帧率:Frame rate应用绘制轮询频率:SM应用绘制超时(跳帧)的次数:Aggregate frame stats、Jankiness count、Skipped frames应用绘制超时(跳帧)的幅度:Aggregate frame stats、Max accumulated frames、Skipped frames特别说明:与 SurfaceFlinger 类似, Surface也仅在有内容更新时才会进行绘制,因此,绘制频率低并不一定意味着图像显示性能差,有可能是因为当前并没有任何的内容更新所导致。如 SM、Skipped frames 这类指标,由于其基础数据取自 Choreographer,若 某些 Surface 的绘制不依赖于 Choreographer ,则这些指标无法衡量该 Surface 的显示性能。如 Aggregate frame stats、Jankiness count、Max accumulated frames、Frame rate 这类指标, 由于其基础数据仅在硬件绘制(Hardware Rendering)过程中进行统计,属于 HWUI 的功能,所以非硬件绘制的 Surface 自然无法使用这类指标进行衡量。小结评价显示性能的各个指标,可以按其在图像渲染流程中的作用,分为以下两类:系统层级的指标仅有 FPS 一根独苗,它的限制是 Surface 的和合成需要在 SurfaceFlinger中进行;应用层级的指标较多,它们之中又可以分为两类:1) SM、Skipped frames 需要 Surface 依赖 Choreographer进行绘制,才能正常工作;2) Aggregate frame stats、Jankiness count、Max accumulated frames、Frame rate 属于 HWUI 的功能, 需要 Surface 的绘制由 HWUI 进行才能进行分析。Step 2:你从哪儿来——这些指标数值是怎么得到的第一章的内容仅仅是站在整个图像绘制流程的高度来简单分析各个指标的,本章将进一步分析各个指标的基础数据来源以及具体计算方式。基础数据:系统层级(SurfaceFlinger)的合成(上屏)的次数前面说到,在 Android 系统中,SurfaceFlinger 扮演了系统中所有 Surface 的管理者的角色,当应用程序所对应的 Surface 更新之后,绝大多数的 Surface 都将在 SurfaceFlinger 之中完成了合并的工作之后,最终才会在 Screen 上显示出来。当然, SurfaceFlinger 的执行也是由 VSYNC 信号驱动的,这也决定了每秒钟合成次数的上限就是 60 次。当 SurfaceFlinger 接收到 Surface 更新通知的时候,将会由 SurfaceFlinger::handleMessageRefresh 函数进行处理,其中包含重建可见区域、初始化、合成等步骤。这里,我们主要关注 SurfaceFlinger::doComposition() 这个方法。void SurfaceFlinger::handleMessageRefresh() {\n
if (CC_UNLIKELY(mDropMissedFrames && frameMissed)) {\n
// Latch buffers, but don't send anything to HWC, then signal another\n
// wakeup for the next vsync\n
preComposition();\n
repaintEverything();\n
} else {\n
preComposition();\n
rebuildLayerStacks();\n
setUpHWComposer();\n
doDebugFlashRegions();\n
doComposition(); //重点关注对象\n
postComposition();\n
...\n}\n在 doComposition 中,完成 Surface 的合成之后,都会调用 DisplayDevice::flip(),它会使用变量 mPageFlipCount 统计我们进行合成的次数,这个变量就是我们统计 FPS 的核心原始数据。mPageFlipCount 记录了 SurfaceFlinger 一共进行了多少次合成,也可以简单理解为,SurfaceFlinger 向屏幕提交了多少帧的数据。void SurfaceFlinger::doComposition() {\n
ATRACE_CALL();\n
const bool repaintEverything = android_atomic_and(0, &mRepaintEverything);\n
for (size_t dpy=0 ; dpy&mDisplays.size() ; dpy++) {\n
const sp&DisplayDevice&& hw(mDisplays[dpy]);\n
if (hw-&isDisplayOn()) {\n
hw-&flip(hw-&swapRegion);//重点关注对象\n
// inform the h/w that we're done compositing\n
hw-&compositionComplete();\n
postFramebuffer();\n}\nvoid DisplayDevice::flip(const Region& dirty) const {\n
mPageFlipCount++;\n}\n不仅如此, Android 还为我们获取这个基础数据提供了比较方便的方法。通过执行 adb 命令:service call SurfaceFlinger 1013,我们就可以得出当前的 mPageFlipCount。C:\\Users\\xiaosongluo&adb shell\nshell@cancro:/ $ su\nsu\nroot@cancro:/ # service call SurfaceFlinger 1013\nservice call SurfaceFlinger 1013\nResult: Parcel(00aea4f4
'....')\nFPS 的计算方法根据 FPS 的定义,我们不难逆推得出 FPS 的计算方法:在 t1 时刻获取 mPageFlipCount 的数值 v1,在在 t2时刻获取 mPageFlipCount 的数值 v2,FPS 的计算公式:FPS = (v2 - v1) / (t2 - t1);\n需要注意的是:mPageFlipCount 的原始数据是 16 进制的,一般而言计算之前需要先进行进制转换。基础数据:应用层级(Surface)的绘制过程中每一帧的关键时间点(FrameInfo)请大家先注意 FrameInfo 是由 Android 6.0(具体来讲是 Android M Preview) 引入到 HWUI 模块中的统计功能。 因此,目前来讲绝大多数系统上的大多数应用都暂时无法获取这一基础数据。不过 This IsTheFuture。我们再来仔细瞧瞧 Google 给出的显示性能测试的十全大补丸
。其中,特别值得关注的是 adb shell dumpsys gfxinfo
framestats 这一条命令。通过这条命令,我们获取每一帧绘制过程中每个关键节点的耗时情况,从而仔细的分析潜在的性能问题。不得不说,按照 Google 给出的这种测试方法进行测试得到的显示性能数据是非常全面的。这些基础数据都是记录在
在doFrame()时进行记录。相关的主要源码如下://源码:FrameInfo.cpp\n#include \"FrameInfo.h\"\n#include &cstring&\n\nnamespace android {\n
namespace uirenderer {\n
const std::string FrameInfoNames[] = {\n
\"Flags\",\n
\"IntendedVsync\",\n
\"Vsync\",\n
\"OldestInputEvent\",\n
\"NewestInputEvent\",\n
\"HandleInputStart\",\n
\"AnimationStart\",\n
\"PerformTraversalsStart\",\n
\"DrawStart\",\n
\"SyncQueued\",\n
\"SyncStart\",\n
\"IssueDrawCommandsStart\",\n
\"SwapBuffers\",\n
\"FrameCompleted\",\n
void FrameInfo::importUiThreadInfo(int64_t* info) {\n
memcpy(mFrameInfo, info, UI_THREAD_FRAME_INFO_SIZE * sizeof(int64_t));\n
} /* namespace uirenderer */\n} /* namespace android */\nAggregate frame stats 指标的计算方法首先需要说明的是 Aggregate frame stats 不是一个指标,而是一系列指标集合。我们来看一个具体的 Aggregate frame stats 的例子:Stats since: nsTotal frames rendered: 82189Janky frames: 3%)90th percentile: 34ms95th percentile: 42ms99th percentile: 69msNumber Missed Vsync: 4706Number High input latency: 142Number Slow UI thread: 17270Number Slow bitmap uploads: 1542Number Slow draw: 23342以上统计信息的实现可以详见源码:在 Android M 以上的系统上,上述信息的获取十分方便(事实上也只有这些系统能够获取这些信息)。仅需要执行以下命令即可:adb shell dumpsys gfxinfo &PACKAGE_NAME&\nJankiness count、Max accumulated frames、Frame rate 指标的计算方法首先需要说明的是:Jankiness count、Max accumulated frames、Frame rate 与 Aggregate frame stats的基础数据并不一致,它们的基础属于来源于 gfxinfo(Profile data in ms)。只是在 Android M 中 gfxinfo(Profile data in ms) 的基础数值来源于 FrameInfo,详见源码:。但在更早的系统之上, gfxinfo(Profile data in ms) 的数值也可以获取。这里需要特别指出的是, gfxinfo(Profile data in ms)只保存了 Surface 最近渲染的128帧的信息,因此,Jankiness count、Max accumulated frames、Frame rate 也仅仅是针对这 128 帧数据所计算出来的结果,它们的具体含义分别是:Jankiness count:根据相邻两帧绘制时间的差值,“估计”是否存在跳帧并进行跳帧次数的统计;Max accumulated frames: 根据相邻两帧绘制时间的差值,“估计”这 128 帧绘制过程中可能形成的最大连续跳帧数;Frame rate:计算所得平均(绘制)帧率。如果你对具体的计算过程感兴趣,可以参考详见源码:基础数据:应用层级(Surface)的绘制过程中每一帧的关键时间点(Choreographer)先说一句有点绕口的话: Choreographer 是依据 Choreographer 绘制的 Surface 在 UI 绘制过程中最为核心的机制。Choreographer 的工作机制简单来说就是,使用者首先通过 postCallback 在 Choreographer 中设置的自己回调函数:CALLBACK_INPUT:优先级最高,和输入事件处理有关。CALLBACK_ANIMATION:优先级其次,和Animation的处理有关。CALLBACK_TRAVERSAL:优先级最低,和UI等控件绘制有关。那么,当 Choreographer 接收到 VSYNC 信号时,Choreographer 会调用 doFrame 函数依次对上述借口进行回调,从而进行渲染。那么显然,doFrame
的执行效率(次数、频率)也就是我们需要的显示性能数据。而这样的基础数据,Choreographer 自身也进行了记录。如下面代码中, jitterNanos 记录了绘制前后两帧所间隔的时间差, 而 skippedFrames 则记录了 jitterNanos 这段时间 doFrame 错过了多少个 VSYNC 信号,即跳过了多少帧。// Set a limit to warn about skipped frames.\n// Skipped frames imply jank.\nprivate static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt(\"debug.choreographer.skipwarning\", 30);\n\nvoid doFrame(long frameTimeNanos, int frame) {\n
final long jitterNanos = startNanos - frameTimeN\n
if (jitterNanos &= mFrameIntervalNanos) {\n
final long skippedFrames = jitterNanos / mFrameIntervalN\n
if (skippedFrames &= SKIPPED_FRAME_WARNING_LIMIT) {\n
Log.i(TAG, \"Skipped \" + skippedFrames + \" frames! \" + \"The application may be doing too much work on its main thread.\");\n
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;\n
frameTimeNanos = startNanos - lastFrameO\n
...\n}\n上述数据的获取并不是那么的直接,所以需要一定的手段。方法一共有三种,都不难:Logcat 方案缺点:该方案需要系统授权 “Adb
Root” 权限,用于修改系统属性;对于丢帧信息只能统计分析,无法进行实时处理。优点:设置完成后,可以获取系统中所有应用各自的绘制丢帧情况(丢帧发生的时间以及连续丢帧的数量)。其实,仔细观察代码,我们就可以注意到 Choreographer 源码中本身就有输出的方案:if (skippedFrames &= SKIPPED_FRAME_WARNING_LIMIT) {\n
Log.i(TAG, \"Skipped \" + skippedFrames + \" frames! \" + \"The application may be doing too much work on its main thread.\");\n}\n唯一阻碍我们获取数值的是:skippedFrames 的数值只有大于 SKIPPED_FRAME_WARNING_LIMIT 才会输出相关的警告。而 SKIPPED_FRAME_WARNING_LIMIT 的数值可以由系统参数 debug.choreographer.skipwarning 来设定。注意:初始条件下,系统中不存在 debug.choreographer.skipwarning 参数,因此 SKIPPED_FRAME_WARNING_LIMIT 将取默认值 30。因此,正常情况下,我们能够看见上诉 Log 出现的机会极少。因此,如果我们修改(设定)系统属性 debug.choreographer.skipwarning 为 1,Logcat 中将打印出每一次丢帧的Log。需要说明的是,由于为 SKIPPED_FRAME_WARNING_LIMIT 赋值的代码段由 Zygote 在系统启动阶段加载,而其他应用都是在拷贝复用 Zygote 中的设定,因此设定系统属性后需要重启 Zygote 才能使得上述设定生效。具体的设置方法如下:setprop debug.choreographer.skipwarning 1\nsetprop ctl.re setprop ctl.restart zygote\n设定完成以后,我们可以直接通过 Logcat 中的信息得到系统中所有应用的绘制丢帧信息,包括丢帧发生的时间以及连续丢帧的数量。不过由于 Logcat 信息的滞后性,以上信息我们几乎只能进行在测试完成后进行统计分析,而无法进行实时处理。Choreographer.FrameCallback 方案缺点:该方案需要将测试代码与待测应用打包在一起,因此理论上仅能测试自己开发的应用。优点:可以对丢帧信息进行实时处理我们先来看看
的定义。Implement this interface to receive a callback when a new display frame is being rendered. The callback is invoked on theLooper thread to which the Choreographeris attached.通过这个接口,我们可以在每一帧被渲染的时候记录下它开始渲染的时间,这样在下一帧被处理是,我们不仅可以判断上一帧在渲染过程中是否出现掉帧,而整个过程都是实时处理的,这为我们可以及时获取相关的调用栈信息来辅助定位潜在的性能缺陷有极大的帮助。代码注入方案缺点:该方案需要通过注入程序为指定应用注入测试代码,因此需要系统为注入程序授权 “应用Root” 权限。优点:与 Choreographer.FrameCallback 方案一致。该方案可以简单理解为通过注入的方式来实现与 Choreographer.FrameCallback 方案一样的目的。因此,这里我们主要讨论两者在实现方式上的区别。显而易见,我们需要注入的对象是 Choreographer ,因此理论上任何第三方应用都是可以被注入的。但是随着 Android 系统对”应用Root” 权限管理越来越严格,所以该方案可用的范围越来越小。SM 指标的计算方法根据定义,SM 其实类似于 FPS,它被设计为可以衡量应用平均每秒执行 doFrame() 的次数。我们可以认为它是在衡量 Surface 渲染轮询的次数。针对 Logcat 方案,我们只需统计测试过程中目标进程一共掉了多少帧,由于对于绝大多数应用在没有丢帧的情况下会针对每一次 VSYNC 信号执行一次 doFrame(),而 VSYNC 绝大多数情况下每秒会触发 60 次,因此我们可以反向计算得出 SM 的数值:SM = (60* totalSeconds - totalSkippedFrames) / totalS\n针对 Choreographer.FrameCallback 方案 以及 代码注入方案,我们需要在代码中自己进行统计输出(可以是设计成实时的,也可以设计成测试结束后进行统计计算的)。Skipped frames 指标的计算方法这个指标的就是指当前应用在丢帧发生时的丢帧帧数。针对 Logcat 方案, 该数值直接在 Logcat 中输出,并且带有时间信息。04-18 16:31:24.957 I/Choreographer(24164): Skipped 4 frames!
The application may be doing too much work on its main thread.\n04-18 16:31:25.009 I/Choreographer(24164): Skipped 2 frames!
The application may be doing too much work on its main thread.\n针对 Choreographer.FrameCallback 方案 以及 代码注入方案,我们可能很方便的通过计算前后两帧开始渲染的时间差获得这一数值,同样方便。同样与 Logcat 方案 不同的是,它也是可以设计成实时计算的。小结通过对各个显示性能指标的分析,我们可以知道,虽然目前指标众多,但其实有本质区别的指标确很少:系统层面: 合成(上屏)帧率:FPS应用层面:跳帧次数:Aggregate frame stats、Jankiness count、Skipped frames跳帧幅度:Aggregate frame stats、Max accumulated frames、Skipped frames绘制帧率:Frame rate绘制轮询频率:SM更为重要的是,我们从上述的分析中知道了各个指标都有着自己的优势和不足,这也从根本上决定了它们各自有各自的用法。Step 3:你要到哪儿去——这些指标如何落地来指导优化其实指标的用法也是多种多样的,为了方便讨论,我们仅从、以及数据上报三个方面来讨论各个显示性能指标是如何落地的。日常监控FPS:数据形式最为直观(FPS 是最早的显示性能指标,而且在多个平台中都有着类似的定义),且对系统平台的要求最低(API level 1),游戏、视频等连续绘制的应用可以考虑选用,但不适用于绝大多数非连续绘制的应用;SM:数据形式与 FPS 类似,可以很好的弥补 FPS 无法准确刻画非连续绘制的应用显示性能的缺陷;Aggregate frame stats:除了对系统平台有较高的要求以外,其采集方式最为简单(系统自带功能);Skipped frames:与 Aggregate frame stats 类似, 信息量相对较少,但可适用范围更广特别说明:Jankiness count、Max accumulated frames、Frame rate 只统计了128帧的信息(约2~3秒),而且 Jankiness count、Max accumulated frames对于掉帧情况的计算并非是一个准确值,因此这些指标都不太适用于日常监控举个栗子,笔者服务的某个产品使用如下的一些指标来监控产品与竞品的性能变化情况:\n测试指标\n场景1\n场景2\n场景3\n场景4\nFPS\n58\n58\n58\n58\nSM\n59\n59\n59\n59\nNum of 6+ Skipped Frames\n0\n0\n0\n0\nNum of 3+ Skipped Frames\n0\n0\n2\n0\n备注:Num of x+ Skipped Frames 代表测试过程中发生连续丢 x 帧(及以上)的次数;至于为什么我们选择关注连续丢 3 帧以及连续丢 6 帧的的次数,在【缺陷定位】部分有相关的分析讨论;缺陷定位Skipped frames:基于 Choreographer.FrameCallback 方案实现的 Skipped frames 指标,可以在卡顿出现的时刻获取应用堆栈信息,可以在一定程度上进行特别说明:FrameInfo 相关指标无法直接进行缺陷定位,但 FrameInfo 当中包含了大量详尽的绘制基础数据,对于缺陷定位也有较大帮助;关于缺陷定位过程中连续掉帧阈值的选取,可参考维基百科中提到几个重要的帧率数值:12 fps:由于人类眼睛的特殊生理结构,如果所看画面之帧率高于每秒约10-12帧的时候,就会认为是连贯的24 fps:有声电影的拍摄及播放帧率均为每秒24帧,对一般人而言已算可接受30 fps:早期的高动态电子游戏,帧率少于每秒30帧的话就会显得不连贯,这是因为没有动态模糊使流畅度降低60 fps:在实际体验中,60帧相对于30帧有着更好的体验以上各数据分别对应: 0 帧、1帧、2.5帧、5~6帧。(这就是为啥选择3/6的原因)举个栗子, 笔者的同事万大师(yuwan)基于上述原理自研了一款性能分析工具。该工具在集成于待测应用之后,可以自动保存如下的性能缺陷信息:Frame lost:... (连续丢帧数量,一般我们会设定一个阈值,例如 6 帧以上我们才会进行记录)\nUser action:...(当前用户操作)\nStack trace: ...(当前堆栈信息)\n有了这个工具之后,我们可以收集应用的各个潜在“卡顿”点,用于进一步的分析和优化。数据上报Aggregate frame stats:除了对系统平台有较高的要求以外,其采集方式最为简单(系统自带功能)、数据也比较清晰,相信基于这类指标实现性能数据上报是特别方便的Skipped frames :基于 Choreographer.FrameCallback 方案实现的 Skipped frames 指标,采集方式简单,实现基础性能数据上报、卡顿数据上报也是很方便的这方面应用,笔者所服务的产品暂时没有涉及,就不举例子了。如果各位观众感兴趣,建议参考 Android ANR 的设计理念。 小结发现了没有 Skipped frames 的用处很大有没有? 而且通读全篇,你会发现 Aggregate frame stats、Jankiness count、Max accumulated frames 这些指标都有提供类似的功能。至于为什么,这就不是本文需要讨论的内容了,如果大家比较感兴趣,笔者这里给出两份相关的链接以供各位参考:友情附赠: 现有显示性能指标对比本来写到这里本文的主要内容就应该结束了。但是如果不对比一下显示性能指标神马的,总会让人觉得缺少了一些什么。友情提示:下述内容相对主观,建议各位读者依据项目情况自行进行选择。\n指标名称\n指标意义\n基础数据来源\n采集方式\n适用系统\n适用应用\n用途\nFPS\n系统合成帧率\nSurfaceFlinger\nadb shell\n略\n略\n监控\nAggregate frame stats\n应用跳帧次数、幅度\nFrameInfo\nadb shell\n最低23\nHW Rendering\n监控/上报\nJankiness count\n(估算)应用跳帧次数\nFrameInfo(128帧)\nadb shell\n略\nHW Rendering\n定位\nMax accumulated frames\n(估算)应用跳帧幅度\nFrameInfo(128帧)\nadb shell\n略\nHW Rendering\n定位\nFrame rate\n应用绘制帧率\nFrameInfo(128帧)\nadb shell\n略\nHW Rendering\n定位\nSM\n应用绘制轮询频率\nChoreographer\n多种方式\n最低16\nSW/HW Rendering 及 部分 OpenGL Rendering\n监控\nSkipped frames\n应用跳帧次数、幅度\nChoreographer\n多种方式\n最低 16\nSW/HW Rendering 及 部分 OpenGL Rendering\n监控/定位/上报\n更多精彩内容欢迎关注的微信公众账号: 是一款专为移动开发者打造的工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条根据根因合并分类,每日日报会列出影响用户数最多的,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧…","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T19:06:17+08:00","url":"/p/","title":"【Bugly干货分享】那些年我们用过的显示性能指标","summary":" 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处。 前言:那些年我们用过的显示性能指标注:Google 在自己文章中用了 Display Performance 来描…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":0,"likesCount":4},"next":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"HTML5"},{"url":"/topic/","id":"","name":"视频直播"},{"url":"/topic/","id":"","name":"前端开发"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"移动端用户反馈神器","isFollowing":false,"hash":"133d72fca32c969f1f84d88abd96752d","uid":24,"isOrg":false,"slug":"bugly","isFollowed":false,"description":"腾讯Bugly,面向移动开发者提供最专业的Crash监控、崩溃分析等质量跟踪服务,帮助您修复用户的每一次Crash!","name":"干掉crash","profileUrl":"/people/bugly","avatar":{"id":"70a08c690f3ea399d656","template":"/{id}_{size}.png"},"isOrgWhiteList":false},"column":{"slug":"bugly","name":"遇见"},"content":" 本文来自于,非经作者同意,请勿转载,原文地址: 视频直播这么火,再不学就 out 了。 为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的技术点,包括但不限于前端技术。1. H5 到底能不能做视频直播? 当然可以, H5 火了这么久,涵盖了各个方面的技术。 对于视频录制,可以使用强大的 webRTC(Web Real-Time Communication)是一个支持网页浏览器进行实时语音对话或视频对话的技术,缺点是只在 PC 的 chrome 上支持较好,移动端支持不太理想。 对于视频播放,可以使用 HLS(HTTP Live Streaming)协议播放直播流, ios 和 android 都天然支持这种协议,配置简单,直接使用 video 标签即可。webRTC 兼容性:video 标签播放 hls 协议视频: &video controls autoplay&
http://10.66.69.77:8080/hls/mystream.m3u8\" type=\"application/vnd.apple.mpegurl\" /&
&p class=\"warning\"&Your browser does not support HTML5 video.&/p&
\n &/video& \n2. 到底什么是 HLS 协议? 简单讲就是把整个流分成一个个小的,基于 HTTP 的文件来下载,每次只下载一些,前面提到了用于 H5 播放直播视频时引入的一个 .m3u8 的文件,这个文件就是基于 HLS 协议,存放视频流元数据的文件。 每一个 .m3u8 文件,分别对应若干个 ts 文件,这些 ts 文件才是真正存放视频的数据,m3u8 文件只是存放了一些 ts 文件的配置信息和相关路径,当视频播放时,.m3u8 是动态改变的,video 标签会解析这个文件,并找到对应的 ts 文件来播放,所以一般为了加快速度,.m3u8 放在 web 服务器上,ts 文件放在 cdn 上。 .m3u8 文件,其实就是以 UTF-8 编码的 m3u 文件,这个文件本身不能播放,只是存放了播放信息的文本文件: #EXTM3U
m3u文件头\n #EXT-X-MEDIA-SEQUENCE
第一个TS分片的序列号\n #EXT-X-TARGETDURATION
每个分片TS的最大的时长\n #EXT-X-ALLOW-CACHE
是否允许cache\n #EXT-X-ENDLIST
m3u8文件结束符\n #EXTINF
指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效\n mystream-12.ts\nts 文件:HLS 的请求流程是: 1 http 请求 m3u8 的 url。 2 服务端返回一个 m3u8 的播放列表,这个播放列表是实时更新的,一般一次给出5段数据的 url。 3 客户端解析 m3u8 的播放列表,再按序请求每一段的 url,获取 ts 数据流。简单流程:3. HLS 直播延时 我们知道 hls 协议是将直播流分成一段一段的小段视频去下载播放的,所以假设列表里面的包含5个 ts 文件,每个 TS 文件包含5秒的视频内容,那么整体的延迟就是25秒。因为当你看到这些视频时,主播已经将视频录制好上传上去了,所以时这样产生的延迟。当然可以缩短列表的长度和单个 ts 文件的大小来降低延迟,极致来说可以缩减列表长度为1,并且 ts 的时长为1s,但是这样会造成请求次数增加,增大服务器压力,当网速慢时回造成更多的缓冲,所以苹果官方推荐的ts时长时10s,所以这样就会大改有30s的延迟。参考资料:4. 视频直播的整个流程是什么?当视频直播可大致分为: 1 视频录制端:一般是电脑上的音视频输入设备或者手机端的摄像头或者麦克风,目前以移动端的手机视频为主。 2 视频播放端:可以是电脑上的播放器,手机端的 native 播放器,还有就是 h5 的 video 标签等,目前还是已手机端的 native 播放器为主。 3 视频服务器端:一般是一台 nginx 服务器,用来接受视频录制端提供的视频源,同时提供给视频播放端流服务。简单流程:5. 怎样进行音视频采集? 当首先明确几个概念:视频编码:所谓视频编码就是指通过特定的压缩技术,将某个视频格式的文件转换成另一种视频格式文件的方式,我们使用的 iphone 录制的视频,必须要经过编码,上传,解码,才能真正的在用户端的播放器里播放。编解码标准:视频流传输中最为重要的编解码标准有国际电联的H.261、H.263、H.264,其中 HLS 协议支持 H.264 格式的编码。音频编码:同视频编码类似,将原始的音频流按照一定的标准进行编码,上传,解码,同时在播放器里播放,当然音频也有许多编码标准,例如 PCM 编码,WMA 编码,AAC 编码等等,这里我们 HLS 协议支持的音频编码方式是AAC编码。 下面将利用 ios 上的摄像头,进行音视频的数据采集,主要分为以下几个步骤: 1 音视频的采集,ios 中,利用 AVCaptureSession和AVCaptureDevice 可以采集到原始的音视频数据流。 2 对视频进行 H264 编码,对音频进行 AAC 编码,在 ios 中分别有已经封装好的编码库来实现对音视频的编码。 3 对编码后的音、视频数据进行组装封包; 4 建立 RTMP 连接并上推到服务端。 ps:由于编码库大多使用 c 语言编写,需要自己使用时编译,对于 ios,可以使用已经编译好的编码库。x264编码:faac编码:ffmpeg编码: 关于如果想给视频增加一些特殊效果,例如增加滤镜等,一般在编码前给使用滤镜库,但是这样也会造成一些耗时,导致上传视频数据有一定延时。简单流程:6. 前面提到的 ffmpeg 是什么? 和之前的 x264 一样,ffmpeg 其实也是一套编码库,类似的还有 Xvid,Xvid 是基于 MPEG4 协议的编解码器,x264是基于 H.264 协议的编码器, ffmpeg 集合了各种音频,视频编解码协议,通过设置参数可以完成基于 MPEG4,H.264 等协议的编解码,demo 这里使用的是 x264 编码库。7. 什么是 RTMP? Real Time Messaging Protocol(简称 RTMP)是 Macromedia 开发的一套视频直播协议,现在属于 Adobe。和 HLS 一样都可以应用于视频直播,区别是 RTMP 基于 flash 无法在 ios 的浏览器里播放,但是实时性比 HLS 要好。所以一般使用这种协议来上传视频流,也就是视频流推送到服务器。 这里列举一下 hls 和 rtmp 对比:8. 推流 简所谓推流,就是将我们已经编码好的音视频数据发往视频流服务器中,一般常用的是使用 rtmp 推流,可以使用第三方库 librtmp-iOS 进行推流,librtmp 封装了一些核心的 api 供使用者调用,如果觉得麻烦,可以使用现成的 ios 视频推流sdk,也是基于 rtmp 的,9. 推流服务器搭建 简简单的推流服务器搭建,由于我们上传的视频流都是基于 rtmp 协议的,所以服务器也必须要支持 rtmp 才行,大概需要以下几个步骤: 1 安装一台 nginx 服务器。 2 安装 nginx 的 rtmp 扩展,目前使用比较多的是 3 配置 nginx 的 conf 文件: rtmp {
listen 1935;
#监听的端口\n\n
chunk_size 4000;
application hls {
#rtmp推流请求路径\n
hls_path /usr/local/var/www/
hls_fragment 5s;
\n 4 重启 nginx,将 rtmp 的推流地址写为 rtmp://ip:1935/hls/mystream,其中 hls_path 表示生成的 .m3u8 和 ts 文件所存放的地址,hls_fragment 表示切片时长,mysteam 表示一个实例,即将来要生成的文件名可以先自己随便设置一个。更多配置可以参考: 根据以上步骤基本上已经实现了一个支持 rtmp 的视频服务器了。10. 在 html5 页面进行播放直播视频? 简单来说,直接使用 video 标签即可播放 hls 协议的直播视频: &video autoplay webkit-playsinline&
http://10.66.69.77:8080/hls/mystream.m3u8\" type=\"application/vnd.apple.mpegurl\" /&
&p class=\"warning\"&Your browser does not support HTML5 video.&/p&
\n &/video& \n 需要注意的是,给 video 标签增加 webkit-playsinline 属性,这个属性是为了让 video 视频在 ios 的 uiwebview 里面可以不全屏播放,默认 ios 会全屏播放视频,需要给 uiwebview 设置 allowsInlineMediaPlayback=YES。 业界比较成熟的 videojs,可以根据不同平台选择不同的策略,例如 ios 使用 video 标签,pc 使用 flash 等。11. 坑点总结 简根据以上步骤,笔者写了一个 demo,从实现 ios 视频录制,采集,上传,nginx 服务器下发直播流,h5 页面播放直播视频者一整套流程,总结出以下几点比较坑的地方: 1 在使用 AVCaptureSession 进行采集视频时,需要实现 AVCaptureVideoDataOutputSampleBufferDelegate 协议,同时在- (void)captureOutput:(AVCaptureOutput )captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection )connection 捕获到视频流,要注意的是 didOutputSampleBuffer 这个方法不是 didDropSampleBuffer 方法,后者只会触发一次,当时开始写的是 didDropSampleBuffer 方法,差了半天才发现方法调用错了。 2 在使用 rtmp 推流时,rmtp 地址要以 rtmp:// 开头,ip 地址要写实际 ip 地址,不要写成 localhost,同时要加上端口号,因为手机端上传时是无法识别 localhos t的。 这里后续会补充上一些坑点,有的需要贴代码,这里先列这么多。12. 业界支持 目前,腾讯云,百度云,阿里云都已经有了基于视频直播的解决方案,从视频录制到视频播放,推流,都有一系列的 sdk 可以使用,缺点就是需要收费,如果可以的话,自己实现一套也并不是难事哈。 demo地址: 参考资料: 更多精彩内容欢迎关注bugly的微信公众账号:weixinBugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条
根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T16:09:58+08:00","url":"/p/","title":"【腾讯bugly干货分享】HTML 5 视频直播一站式扫盲","summary":"本文来自于,非经作者同意,请勿转载,原文地址: 视频直播这么火,再不学就 out 了。 为了紧跟潮流,本文将向大家介绍一下视频直播中的基本流程和主要的技术点,包括但不限于前端技术。1. H5 到底能不能做视频直播…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":0,"likesCount":1}},"annotationDetail":null,"commentsCount":3,"likesCount":9,"FULLINFO":true}},"User":{"bugly":{"isFollowed":false,"name":"干掉crash","headline":"腾讯Bugly,面向移动开发者提供最专业的Crash监控、崩溃分析等质量跟踪服务,帮助您修复用户的每一次Crash!","avatarUrl":"/70a08c690f3ea399d656_s.png","isFollowing":false,"type":"people","slug":"bugly","bio":"移动端用户反馈神器","hash":"133d72fca32c969f1f84d88abd96752d","uid":24,"isOrg":false,"description":"腾讯Bugly,面向移动开发者提供最专业的Crash监控、崩溃分析等质量跟踪服务,帮助您修复用户的每一次Crash!","profileUrl":"/people/bugly","avatar":{"id":"70a08c690f3ea399d656","template":"/{id}_{size}.png"},"isOrgWhiteList":false,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{},"columns":{"next":{},"bugly":{"following":false,"canManage":false,"href":"/api/columns/bugly","name":"遇见","creator":{"slug":"teng-xun-bugly"},"url":"/bugly","slug":"bugly","avatar":{"id":"e1ff32e9e1114d0ceba728a65623eed8","template":"/{id}_{size}.jpeg"}}},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{}}

我要回帖

更多关于 apktool反编译apk 的文章

 

随机推荐