-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdb.json
More file actions
1 lines (1 loc) · 758 KB
/
db.json
File metadata and controls
1 lines (1 loc) · 758 KB
1
{"meta":{"version":1,"warehouse":"5.0.1"},"models":{"Asset":[{"_id":"source/CNAME","path":"CNAME","modified":0,"renderable":0},{"_id":"source/imgs/29f3ac36.png","path":"imgs/29f3ac36.png","modified":0,"renderable":0},{"_id":"source/imgs/WechatIMG2.jpeg","path":"imgs/WechatIMG2.jpeg","modified":0,"renderable":0},{"_id":"source/imgs/android1.png","path":"imgs/android1.png","modified":0,"renderable":0},{"_id":"source/imgs/android2.png","path":"imgs/android2.png","modified":0,"renderable":0},{"_id":"source/imgs/android3.png","path":"imgs/android3.png","modified":0,"renderable":0},{"_id":"source/imgs/appstart2.png","path":"imgs/appstart2.png","modified":0,"renderable":0},{"_id":"source/imgs/appstart.png","path":"imgs/appstart.png","modified":0,"renderable":0},{"_id":"source/imgs/b6f8dc13.png","path":"imgs/b6f8dc13.png","modified":0,"renderable":0},{"_id":"source/imgs/clion_engine.png","path":"imgs/clion_engine.png","modified":0,"renderable":0},{"_id":"source/imgs/filter.png","path":"imgs/filter.png","modified":0,"renderable":0},{"_id":"source/imgs/splash.png","path":"imgs/splash.png","modified":0,"renderable":0},{"_id":"source/imgs/state.png","path":"imgs/state.png","modified":0,"renderable":0},{"_id":"source/imgs/tokio_classes.png","path":"imgs/tokio_classes.png","modified":0,"renderable":0},{"_id":"themes/next/source/css/main.styl","path":"css/main.styl","modified":0,"renderable":1},{"_id":"themes/next/source/images/algolia_logo.svg","path":"images/algolia_logo.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/apple-touch-icon-next.png","path":"images/apple-touch-icon-next.png","modified":0,"renderable":1},{"_id":"themes/next/source/images/avatar.gif","path":"images/avatar.gif","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-by-nc-nd.svg","path":"images/cc-by-nc-nd.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-by-nc-sa.svg","path":"images/cc-by-nc-sa.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-by-nc.svg","path":"images/cc-by-nc.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-by-nd.svg","path":"images/cc-by-nd.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-by-sa.svg","path":"images/cc-by-sa.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-by.svg","path":"images/cc-by.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/cc-zero.svg","path":"images/cc-zero.svg","modified":0,"renderable":1},{"_id":"themes/next/source/images/favicon-16x16-next.png","path":"images/favicon-16x16-next.png","modified":0,"renderable":1},{"_id":"themes/next/source/images/favicon-32x32-next.png","path":"images/favicon-32x32-next.png","modified":0,"renderable":1},{"_id":"themes/next/source/images/logo.svg","path":"images/logo.svg","modified":0,"renderable":1},{"_id":"themes/next/source/js/algolia-search.js","path":"js/algolia-search.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/bookmark.js","path":"js/bookmark.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/local-search.js","path":"js/local-search.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/motion.js","path":"js/motion.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/next-boot.js","path":"js/next-boot.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/utils.js","path":"js/utils.js","modified":0,"renderable":1},{"_id":"themes/next/source/lib/anime.min.js","path":"lib/anime.min.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/schemes/muse.js","path":"js/schemes/muse.js","modified":0,"renderable":1},{"_id":"themes/next/source/js/schemes/pisces.js","path":"js/schemes/pisces.js","modified":0,"renderable":1},{"_id":"themes/next/source/lib/velocity/velocity.min.js","path":"lib/velocity/velocity.min.js","modified":0,"renderable":1},{"_id":"themes/next/source/lib/velocity/velocity.ui.min.js","path":"lib/velocity/velocity.ui.min.js","modified":0,"renderable":1},{"_id":"themes/next/source/lib/font-awesome/css/all.min.css","path":"lib/font-awesome/css/all.min.css","modified":0,"renderable":1},{"_id":"themes/next/source/lib/font-awesome/webfonts/fa-brands-400.woff2","path":"lib/font-awesome/webfonts/fa-brands-400.woff2","modified":0,"renderable":1},{"_id":"themes/next/source/lib/font-awesome/webfonts/fa-regular-400.woff2","path":"lib/font-awesome/webfonts/fa-regular-400.woff2","modified":0,"renderable":1},{"_id":"themes/next/source/lib/font-awesome/webfonts/fa-solid-900.woff2","path":"lib/font-awesome/webfonts/fa-solid-900.woff2","modified":0,"renderable":1}],"Cache":[{"_id":"source/CNAME","hash":"da39a3ee5e6b4b0d3255bfef95601890afd80709","modified":1748870133844},{"_id":"source/_posts/Android渲染之Surface与ANativeWindow.md","hash":"223c02266874f9729f5731baa5ed1fce35793b70","modified":1748870133844},{"_id":"source/_posts/MacOS环境下Flutter Engine编译纪要.md","hash":"cb9ba17d7848cda0227a119408c9940e30839ea1","modified":1748870133844},{"_id":"source/_posts/rokio源码实现简单分析.md","hash":"35e5a5afa3456d7c6a0a9ba9a57aa15cddd34717","modified":1748870133845},{"_id":"source/_posts/一种有效管控APP隐私权限的解决方案.md","hash":"1a44e746c16f0a29e606ea9ba18a0964d8ee50cb","modified":1748870133845},{"_id":"source/_posts/介绍两款androidX迁移利器.md","hash":"a52e64bc6b32ba38af62152e4360a1964d3e698a","modified":1748870133845},{"_id":"source/_posts/埋点系统之可信数据持续集成实践.md","hash":"96c9106b6094e0d182b3de969a6597dcdf570808","modified":1748870133845},{"_id":"source/_posts/小功能之回退首页可行性解决方案探索.md","hash":"6eb2b0c444e2e551c84a2bba787b5ecc0400b847","modified":1748870133845},{"_id":"source/_posts/快速开闭闪光灯实现与风险规避.md","hash":"4322dab248f90defe3e4856d30e1fdf21cb3a0af","modified":1748870133845},{"_id":"source/.DS_Store","hash":"da6df1108f62a3da93e23a4fe8123f0ded5fa8e7","modified":1748854141510},{"_id":"source/_posts/插件化之AAPT客户化.md","hash":"47eaa8a080898a2cf43ae637eabbc481b43f5858","modified":1748870133845},{"_id":"source/_posts/插件化之启动优化实践.md","hash":"7d9fd37649d2194e883885d398c3bc6ff06d7cfc","modified":1748870133845},{"_id":"source/_posts/插件化之插件混淆的可行性探索.md","hash":"840b4394da8580a0b2d073863e78e5768d7790f7","modified":1748870133846},{"_id":"source/_posts/治理令人头痛的pthread-create-OutOfMemoryError错误.md","hash":"0c0172f7b7d78d8988ce9bf74dbbbb0f55296990","modified":1748870133846},{"_id":"source/categories/index.md","hash":"cf91f1ba047d2927a3f13570aabfcae849c2a435","modified":1748870133846},{"_id":"source/about/index.md","hash":"227bdf377dedb4e7fdb83b7aee09b6f9e5dc9e49","modified":1748870133846},{"_id":"source/imgs/appstart.png","hash":"6a61ff23be62139e69021c07e1e9ecf7e28ae5a4","modified":1748870133851},{"_id":"source/imgs/appstart2.png","hash":"148765204232192f72085ccb14add09f40c2c1d4","modified":1748870133851},{"_id":"source/tags/index.md","hash":"1be66178692161bdf44b61785a218ed19b51e2e4","modified":1748870133858},{"_id":"source/imgs/WechatIMG2.jpeg","hash":"765ef9e59c74f0a19d7e132b347d8dec10cdebcd","modified":1748870133848},{"_id":"source/imgs/b6f8dc13.png","hash":"9f81560769ad7efb65dd08b4cd8d3dc9461fe3a9","modified":1748870133851},{"_id":"source/imgs/splash.png","hash":"0dc286274b8b99e34a2cf48a57ff8d1d70f7f68e","modified":1748870133856},{"_id":"source/imgs/filter.png","hash":"c88572ae58d4e532e408ec11ee1a44078fc7d6c5","modified":1748870133856},{"_id":"source/imgs/android2.png","hash":"9ba7e6c2aed48890e0d792c436a9223b301fcff5","modified":1748870133850},{"_id":"source/imgs/android3.png","hash":"c71843dd8e529d3247bf8da9d7eacd8258209130","modified":1748870133850},{"_id":"source/imgs/state.png","hash":"818540e78ae0927098803b7d54fb9060e3e6212a","modified":1748870133857},{"_id":"source/imgs/tokio_classes.png","hash":"f5556753c15c5f9ad766befc53593f01c1705a72","modified":1748870133857},{"_id":"source/imgs/29f3ac36.png","hash":"a61f916823905241ad3b5d5f0593f3396170c504","modified":1748870133847},{"_id":"source/imgs/android1.png","hash":"59e048b3889a79f6d22a73de904f31a18e87ebe6","modified":1748870133849},{"_id":"themes/next/.editorconfig","hash":"8570735a8d8d034a3a175afd1dd40b39140b3e6a","modified":1748870133858},{"_id":"themes/next/.eslintrc.json","hash":"cc5f297f0322672fe3f684f823bc4659e4a54c41","modified":1748870133858},{"_id":"themes/next/.gitattributes","hash":"a54f902957d49356376b59287b894b1a3d7a003f","modified":1748870133858},{"_id":"themes/next/.gitignore","hash":"56f3470755c20311ddd30d421b377697a6e5e68b","modified":1748870133859},{"_id":"themes/next/LICENSE.md","hash":"18144d8ed58c75af66cb419d54f3f63374cd5c5b","modified":1748870133859},{"_id":"themes/next/.travis.yml","hash":"ecca3b919a5b15886e3eca58aa84aafc395590da","modified":1748870133859},{"_id":"themes/next/.stylintrc","hash":"2cf4d637b56d8eb423f59656a11f6403aa90f550","modified":1748870133859},{"_id":"themes/next/README.md","hash":"9b4b7d66aca47f9c65d6321b14eef48d95c4dff1","modified":1748870133859},{"_id":"themes/next/crowdin.yml","hash":"e026078448c77dcdd9ef50256bb6635a8f83dca6","modified":1748870133860},{"_id":"themes/next/package.json","hash":"62fad6de02adbbba9fb096cbe2dcc15fe25f2435","modified":1748870133868},{"_id":"themes/next/gulpfile.js","hash":"1b4fc262b89948937b9e3794de812a7c1f2f3592","modified":1748870133862},{"_id":"themes/next/.github/CODE_OF_CONDUCT.md","hash":"aa4cb7aff595ca628cb58160ee1eee117989ec4e","modified":1748870133858},{"_id":"themes/next/.github/PULL_REQUEST_TEMPLATE.md","hash":"1a435c20ae8fa183d49bbf96ac956f7c6c25c8af","modified":1748870133858},{"_id":"themes/next/.github/issue-close-app.yml","hash":"7cba457eec47dbfcfd4086acd1c69eaafca2f0cd","modified":1748870133858},{"_id":"themes/next/.github/issue_label_bot.yaml","hash":"fca600ddef6f80c5e61aeed21722d191e5606e5b","modified":1748870133859},{"_id":"themes/next/_config.yml","hash":"ccb323cd90b83785844d0217aa6c162d6a1c4ba9","modified":1748870133860},{"_id":"themes/next/.github/lock.yml","hash":"61173b9522ebac13db2c544e138808295624f7fd","modified":1748870133859},{"_id":"themes/next/.github/release-drafter.yml","hash":"3cc10ce75ecc03a5ce86b00363e2a17eb65d15ea","modified":1748870133859},{"_id":"themes/next/.github/mergeable.yml","hash":"0ee56e23bbc71e1e76427d2bd255a9879bd36e22","modified":1748870133859},{"_id":"themes/next/.github/config.yml","hash":"1d3f4e8794986817c0fead095c74f756d45f91ed","modified":1748870133858},{"_id":"themes/next/.github/CONTRIBUTING.md","hash":"e554931b98f251fd49ff1d2443006d9ea2c20461","modified":1748870133858},{"_id":"themes/next/.github/stale.yml","hash":"fdf82de9284f8bc8e0b0712b4cc1cb081a94de59","modified":1748870133859},{"_id":"themes/next/docs/AUTHORS.md","hash":"10135a2f78ac40e9f46b3add3e360c025400752f","modified":1748870133860},{"_id":"themes/next/.github/support.yml","hash":"d75db6ffa7b4ca3b865a925f9de9aef3fc51925c","modified":1748870133859},{"_id":"themes/next/docs/DATA-FILES.md","hash":"cddbdc91ee9e65c37a50bec12194f93d36161616","modified":1748870133860},{"_id":"themes/next/docs/INSTALLATION.md","hash":"af88bcce035780aaa061261ed9d0d6c697678618","modified":1748870133860},{"_id":"themes/next/docs/AGPL3.md","hash":"0d2b8c5fa8a614723be0767cc3bca39c49578036","modified":1748870133860},{"_id":"themes/next/docs/ALGOLIA-SEARCH.md","hash":"c7a994b9542040317d8f99affa1405c143a94a38","modified":1748870133860},{"_id":"themes/next/docs/LICENSE.txt","hash":"368bf2c29d70f27d8726dd914f1b3211cae4bbab","modified":1748870133860},{"_id":"themes/next/docs/LEANCLOUD-COUNTER-SECURITY.md","hash":"94dc3404ccb0e5f663af2aa883c1af1d6eae553d","modified":1748870133860},{"_id":"themes/next/docs/UPDATE-FROM-5.1.X.md","hash":"8b6e4b2c9cfcb969833092bdeaed78534082e3e6","modified":1748870133860},{"_id":"themes/next/docs/MATH.md","hash":"d645b025ec7fb9fbf799b9bb76af33b9f5b9ed93","modified":1748870133860},{"_id":"themes/next/languages/ar.yml","hash":"9815e84e53d750c8bcbd9193c2d44d8d910e3444","modified":1748870133862},{"_id":"themes/next/languages/default.yml","hash":"45bc5118828bdc72dcaa25282cd367c8622758cb","modified":1748870133862},{"_id":"themes/next/languages/es.yml","hash":"c64cf05f356096f1464b4b1439da3c6c9b941062","modified":1748870133862},{"_id":"themes/next/languages/fa.yml","hash":"3676b32fda37e122f3c1a655085a1868fb6ad66b","modified":1748870133862},{"_id":"themes/next/languages/de.yml","hash":"74c59f2744217003b717b59d96e275b54635abf5","modified":1748870133862},{"_id":"themes/next/languages/hu.yml","hash":"b1ebb77a5fd101195b79f94de293bcf9001d996f","modified":1748870133862},{"_id":"themes/next/languages/id.yml","hash":"572ed855d47aafe26f58c73b1394530754881ec2","modified":1748870133862},{"_id":"themes/next/languages/fr.yml","hash":"752bf309f46a2cd43890b82300b342d7218d625f","modified":1748870133862},{"_id":"themes/next/languages/en.yml","hash":"45bc5118828bdc72dcaa25282cd367c8622758cb","modified":1748870133862},{"_id":"themes/next/languages/ja.yml","hash":"0cf0baa663d530f22ff380a051881216d6adcdd8","modified":1748870133862},{"_id":"themes/next/languages/nl.yml","hash":"5af3473d9f22897204afabc08bb984b247493330","modified":1748870133862},{"_id":"themes/next/languages/ko.yml","hash":"0feea9e43cd399f3610b94d755a39fff1d371e97","modified":1748870133862},{"_id":"themes/next/languages/it.yml","hash":"44759f779ce9c260b895532de1d209ad4bd144bf","modified":1748870133862},{"_id":"themes/next/languages/pt.yml","hash":"718d131f42f214842337776e1eaddd1e9a584054","modified":1748870133862},{"_id":"themes/next/languages/pt-BR.yml","hash":"67555b1ba31a0242b12fc6ce3add28531160e35b","modified":1748870133862},{"_id":"themes/next/languages/ru.yml","hash":"e993d5ca072f7f6887e30fc0c19b4da791ca7a88","modified":1748870133862},{"_id":"themes/next/languages/uk.yml","hash":"3a6d635b1035423b22fc86d9455dba9003724de9","modified":1748870133863},{"_id":"themes/next/languages/vi.yml","hash":"93393b01df148dcbf0863f6eee8e404e2d94ef9e","modified":1748870133863},{"_id":"themes/next/languages/tr.yml","hash":"2b041eeb8bd096f549464f191cfc1ea0181daca4","modified":1748870133862},{"_id":"themes/next/languages/zh-HK.yml","hash":"3789f94010f948e9f23e21235ef422a191753c65","modified":1748870133863},{"_id":"themes/next/languages/zh-CN.yml","hash":"a1f15571ee7e1e84e3cc0985c3ec4ba1a113f6f8","modified":1748870133863},{"_id":"themes/next/languages/zh-TW.yml","hash":"8c09da7c4ec3fca2c6ee897b2eea260596a2baa1","modified":1748870133863},{"_id":"themes/next/layout/_layout.swig","hash":"6a6e92a4664cdb981890a27ac11fd057f44de1d5","modified":1748870133863},{"_id":"themes/next/layout/archive.swig","hash":"e4e31317a8df68f23156cfc49e9b1aa9a12ad2ed","modified":1748870133867},{"_id":"themes/next/layout/category.swig","hash":"1bde61cf4d2d171647311a0ac2c5c7933f6a53b0","modified":1748870133867},{"_id":"themes/next/layout/post.swig","hash":"2f6d992ced7e067521fdce05ffe4fd75481f41c5","modified":1748870133868},{"_id":"themes/next/layout/index.swig","hash":"7f403a18a68e6d662ae3e154b2c1d3bbe0801a23","modified":1748870133867},{"_id":"themes/next/layout/page.swig","hash":"db581bdeac5c75fabb0f17d7c5e746e47f2a9168","modified":1748870133868},{"_id":"themes/next/layout/tag.swig","hash":"0dfb653bd5de980426d55a0606d1ab122bd8c017","modified":1748870133868},{"_id":"themes/next/.github/ISSUE_TEMPLATE/feature-request.md","hash":"12d99fb8b62bd9e34d9672f306c9ae4ace7e053e","modified":1748870133858},{"_id":"themes/next/.github/ISSUE_TEMPLATE/other.md","hash":"d3efc0df0275c98440e69476f733097916a2d579","modified":1748870133858},{"_id":"themes/next/scripts/renderer.js","hash":"49a65df2028a1bc24814dc72fa50d52231ca4f05","modified":1748870133869},{"_id":"themes/next/.github/ISSUE_TEMPLATE/question.md","hash":"53df7d537e26aaf062d70d86835c5fd8f81412f3","modified":1748870133858},{"_id":"themes/next/.github/ISSUE_TEMPLATE/bug-report.md","hash":"c3e6b8196c983c40fd140bdeca012d03e6e86967","modified":1748870133858},{"_id":"themes/next/docs/ru/DATA-FILES.md","hash":"0bd2d696f62a997a11a7d84fec0130122234174e","modified":1748870133861},{"_id":"themes/next/docs/ru/INSTALLATION.md","hash":"9c4fe2873123bf9ceacab5c50d17d8a0f1baef27","modified":1748870133861},{"_id":"themes/next/docs/ru/README.md","hash":"85dd68ed1250897a8e4a444a53a68c1d49eb7e11","modified":1748870133861},{"_id":"themes/next/docs/ru/UPDATE-FROM-5.1.X.md","hash":"5237a368ab99123749d724b6c379415f2c142a96","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/CONTRIBUTING.md","hash":"d3f03be036b75dc71cf3c366cd75aee7c127c874","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/CODE_OF_CONDUCT.md","hash":"fb23b85db6f7d8279d73ae1f41631f92f64fc864","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/ALGOLIA-SEARCH.md","hash":"34b88784ec120dfdc20fa82aadeb5f64ef614d14","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/INSTALLATION.md","hash":"579c7bd8341873fb8be4732476d412814f1a3df7","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/LEANCLOUD-COUNTER-SECURITY.md","hash":"8b18f84503a361fc712b0fe4d4568e2f086ca97d","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/MATH.md","hash":"b92585d251f1f9ebe401abb5d932cb920f9b8b10","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/DATA-FILES.md","hash":"ca1030efdfca5e20f9db2e7a428998e66a24c0d0","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/README.md","hash":"c038629ff8f3f24e8593c4c8ecf0bef3a35c750d","modified":1748870133861},{"_id":"themes/next/docs/zh-CN/UPDATE-FROM-5.1.X.md","hash":"d9ce7331c1236bbe0a551d56cef2405e47e65325","modified":1748870133861},{"_id":"themes/next/layout/_macro/sidebar.swig","hash":"71655ca21907e9061b6e8ac52d0d8fbf54d0062b","modified":1748870133863},{"_id":"themes/next/layout/_macro/post-collapse.swig","hash":"9c8dc0b8170679cdc1ee9ee8dbcbaebf3f42897b","modified":1748870133863},{"_id":"themes/next/layout/_macro/post.swig","hash":"090b5a9b6fca8e968178004cbd6cff205b7eba57","modified":1748870133863},{"_id":"themes/next/layout/_partials/footer.swig","hash":"4369b313cbbeae742cb35f86d23d99d4285f7359","modified":1748870133863},{"_id":"themes/next/layout/_partials/comments.swig","hash":"db6ab5421b5f4b7cb32ac73ad0e053fdf065f83e","modified":1748870133863},{"_id":"themes/next/layout/_partials/pagination.swig","hash":"9876dbfc15713c7a47d4bcaa301f4757bd978269","modified":1748870133864},{"_id":"themes/next/layout/_partials/languages.swig","hash":"ba9e272f1065b8f0e8848648caa7dea3f02c6be1","modified":1748870133864},{"_id":"themes/next/layout/_scripts/index.swig","hash":"cea942b450bcb0f352da78d76dc6d6f1d23d5029","modified":1748870133865},{"_id":"themes/next/layout/_partials/widgets.swig","hash":"83a40ce83dfd5cada417444fb2d6f5470aae6bb0","modified":1748870133865},{"_id":"themes/next/layout/_scripts/three.swig","hash":"a4f42f2301866bd25a784a2281069d8b66836d0b","modified":1748870133865},{"_id":"themes/next/layout/_scripts/noscript.swig","hash":"d1f2bfde6f1da51a2b35a7ab9e7e8eb6eefd1c6b","modified":1748870133865},{"_id":"themes/next/layout/_scripts/vendors.swig","hash":"ef38c213679e7b6d2a4116f56c9e55d678446069","modified":1748870133865},{"_id":"themes/next/layout/_third-party/baidu-push.swig","hash":"b782eb2e34c0c15440837040b5d65b093ab6ec04","modified":1748870133866},{"_id":"themes/next/layout/_scripts/pjax.swig","hash":"4d2c93c66e069852bb0e3ea2e268d213d07bfa3f","modified":1748870133865},{"_id":"themes/next/scripts/events/index.js","hash":"5743cde07f3d2aa11532a168a652e52ec28514fd","modified":1748870133868},{"_id":"themes/next/layout/_third-party/index.swig","hash":"70c3c01dd181de81270c57f3d99b6d8f4c723404","modified":1748870133866},{"_id":"themes/next/layout/_third-party/quicklink.swig","hash":"311e5eceec9e949f1ea8d623b083cec0b8700ff2","modified":1748870133867},{"_id":"themes/next/layout/_third-party/rating.swig","hash":"2731e262a6b88eaee2a3ca61e6a3583a7f594702","modified":1748870133867},{"_id":"themes/next/scripts/filters/default-injects.js","hash":"aec50ed57b9d5d3faf2db3c88374f107203617e0","modified":1748870133869},{"_id":"themes/next/scripts/filters/minify.js","hash":"19985723b9f677ff775f3b17dcebf314819a76ac","modified":1748870133869},{"_id":"themes/next/scripts/filters/post.js","hash":"44ba9b1c0bdda57590b53141306bb90adf0678db","modified":1748870133869},{"_id":"themes/next/scripts/filters/front-matter.js","hash":"703bdd142a671b4b67d3d9dfb4a19d1dd7e7e8f7","modified":1748870133869},{"_id":"themes/next/scripts/helpers/font.js","hash":"40cf00e9f2b7aa6e5f33d412e03ed10304b15fd7","modified":1748870133869},{"_id":"themes/next/scripts/filters/locals.js","hash":"b193a936ee63451f09f8886343dcfdca577c0141","modified":1748870133869},{"_id":"themes/next/scripts/helpers/next-config.js","hash":"5e11f30ddb5093a88a687446617a46b048fa02e5","modified":1748870133869},{"_id":"themes/next/scripts/tags/button.js","hash":"8c6b45f36e324820c919a822674703769e6da32c","modified":1748870133869},{"_id":"themes/next/scripts/helpers/next-url.js","hash":"958e86b2bd24e4fdfcbf9ce73e998efe3491a71f","modified":1748870133869},{"_id":"themes/next/scripts/tags/caniuse.js","hash":"94e0bbc7999b359baa42fa3731bdcf89c79ae2b3","modified":1748870133869},{"_id":"themes/next/scripts/tags/group-pictures.js","hash":"d902fd313e8d35c3cc36f237607c2a0536c9edf1","modified":1748870133869},{"_id":"themes/next/scripts/helpers/engine.js","hash":"bdb424c3cc0d145bd0c6015bb1d2443c8a9c6cda","modified":1748870133869},{"_id":"themes/next/scripts/tags/center-quote.js","hash":"f1826ade2d135e2f60e2d95cb035383685b3370c","modified":1748870133869},{"_id":"themes/next/scripts/tags/mermaid.js","hash":"983c6c4adea86160ecc0ba2204bc312aa338121d","modified":1748870133870},{"_id":"themes/next/scripts/tags/note.js","hash":"0a02bb4c15aec41f6d5f1271cdb5c65889e265d9","modified":1748870133870},{"_id":"themes/next/scripts/tags/label.js","hash":"fc5b267d903facb7a35001792db28b801cccb1f8","modified":1748870133870},{"_id":"themes/next/scripts/tags/tabs.js","hash":"93d8a734a3035c1d3f04933167b500517557ba3e","modified":1748870133870},{"_id":"themes/next/scripts/tags/video.js","hash":"e5ff4c44faee604dd3ea9db6b222828c4750c227","modified":1748870133870},{"_id":"themes/next/source/css/_colors.styl","hash":"a8442520f719d3d7a19811cb3b85bcfd4a596e1f","modified":1748870133870},{"_id":"themes/next/scripts/tags/pdf.js","hash":"8c613b39e7bff735473e35244b5629d02ee20618","modified":1748870133870},{"_id":"themes/next/source/css/main.styl","hash":"a3a3bbb5a973052f0186b3523911cb2539ff7b88","modified":1748870133875},{"_id":"themes/next/source/css/_mixins.styl","hash":"e31a557f8879c2f4d8d5567ee1800b3e03f91f6e","modified":1748870133874},{"_id":"themes/next/source/images/apple-touch-icon-next.png","hash":"2959dbc97f31c80283e67104fe0854e2369e40aa","modified":1748870133875},{"_id":"themes/next/source/images/algolia_logo.svg","hash":"ec119560b382b2624e00144ae01c137186e91621","modified":1748870133875},{"_id":"themes/next/source/images/cc-by-nc-sa.svg","hash":"3031be41e8753c70508aa88e84ed8f4f653f157e","modified":1748870133875},{"_id":"themes/next/source/images/avatar.gif","hash":"18c53e15eb0c84b139995f9334ed8522b40aeaf6","modified":1748870133875},{"_id":"themes/next/source/images/cc-by-nc.svg","hash":"8d39b39d88f8501c0d27f8df9aae47136ebc59b7","modified":1748870133875},{"_id":"themes/next/source/images/cc-by-nc-nd.svg","hash":"c6524ece3f8039a5f612feaf865d21ec8a794564","modified":1748870133875},{"_id":"themes/next/source/images/cc-by-sa.svg","hash":"aa4742d733c8af8d38d4c183b8adbdcab045872e","modified":1748870133875},{"_id":"themes/next/source/images/cc-by.svg","hash":"28a0a4fe355a974a5e42f68031652b76798d4f7e","modified":1748870133876},{"_id":"themes/next/source/images/cc-by-nd.svg","hash":"c563508ce9ced1e66948024ba1153400ac0e0621","modified":1748870133875},{"_id":"themes/next/source/images/cc-zero.svg","hash":"87669bf8ac268a91d027a0a4802c92a1473e9030","modified":1748870133876},{"_id":"themes/next/source/images/logo.svg","hash":"d29cacbae1bdc4bbccb542107ee0524fe55ad6de","modified":1748870133876},{"_id":"themes/next/source/images/favicon-16x16-next.png","hash":"943a0d67a9cdf8c198109b28f9dbd42f761d11c3","modified":1748870133876},{"_id":"themes/next/source/images/favicon-32x32-next.png","hash":"0749d7b24b0d2fae1c8eb7f671ad4646ee1894b1","modified":1748870133876},{"_id":"themes/next/source/js/local-search.js","hash":"35ccf100d8f9c0fd6bfbb7fa88c2a76c42a69110","modified":1748870133876},{"_id":"themes/next/source/js/algolia-search.js","hash":"498d233eb5c7af6940baf94c1a1c36fdf1dd2636","modified":1748870133876},{"_id":"themes/next/source/js/motion.js","hash":"72df86f6dfa29cce22abeff9d814c9dddfcf13a9","modified":1748870133876},{"_id":"themes/next/source/js/bookmark.js","hash":"9734ebcb9b83489686f5c2da67dc9e6157e988ad","modified":1748870133876},{"_id":"themes/next/source/js/next-boot.js","hash":"a1b0636423009d4a4e4cea97bcbf1842bfab582c","modified":1748870133876},{"_id":"themes/next/source/js/utils.js","hash":"730cca7f164eaf258661a61ff3f769851ff1e5da","modified":1748870133876},{"_id":"themes/next/layout/_partials/head/head-unique.swig","hash":"000bad572d76ee95d9c0a78f9ccdc8d97cc7d4b4","modified":1748870133863},{"_id":"themes/next/layout/_partials/head/head.swig","hash":"810d544019e4a8651b756dd23e5592ee851eda71","modified":1748870133863},{"_id":"themes/next/source/lib/anime.min.js","hash":"47cb482a8a488620a793d50ba8f6752324b46af3","modified":1748870133876},{"_id":"themes/next/layout/_partials/header/brand.swig","hash":"c70f8e71e026e878a4e9d5ab3bbbf9b0b23c240c","modified":1748870133864},{"_id":"themes/next/layout/_partials/header/menu-item.swig","hash":"9440d8a3a181698b80e1fa47f5104f4565d8cdf3","modified":1748870133864},{"_id":"themes/next/layout/_partials/header/index.swig","hash":"7dbe93b8297b746afb89700b4d29289556e85267","modified":1748870133864},{"_id":"themes/next/layout/_partials/header/sub-menu.swig","hash":"ae2261bea836581918a1c2b0d1028a78718434e0","modified":1748870133864},{"_id":"themes/next/layout/_partials/page/breadcrumb.swig","hash":"c851717497ca64789f2176c9ecd1dedab237b752","modified":1748870133864},{"_id":"themes/next/layout/_partials/page/page-header.swig","hash":"9b7a66791d7822c52117fe167612265356512477","modified":1748870133864},{"_id":"themes/next/layout/_partials/post/post-copyright.swig","hash":"954ad71536b6eb08bd1f30ac6e2f5493b69d1c04","modified":1748870133864},{"_id":"themes/next/layout/_partials/post/post-followme.swig","hash":"ceba16b9bd3a0c5c8811af7e7e49d0f9dcb2f41e","modified":1748870133864},{"_id":"themes/next/layout/_partials/header/menu.swig","hash":"d31f896680a6c2f2c3f5128b4d4dd46c87ce2130","modified":1748870133864},{"_id":"themes/next/layout/_partials/post/post-related.swig","hash":"f79c44692451db26efce704813f7a8872b7e63a0","modified":1748870133864},{"_id":"themes/next/layout/_partials/post/post-footer.swig","hash":"8f14f3f8a1b2998d5114cc56b680fb5c419a6b07","modified":1748870133864},{"_id":"themes/next/layout/_partials/search/index.swig","hash":"2be50f9bfb1c56b85b3b6910a7df27f51143632c","modified":1748870133865},{"_id":"themes/next/layout/_partials/search/localsearch.swig","hash":"f48a6a8eba04eb962470ce76dd731e13074d4c45","modified":1748870133865},{"_id":"themes/next/layout/_partials/search/algolia-search.swig","hash":"48430bd03b8f19c9b8cdb2642005ed67d56c6e0b","modified":1748870133864},{"_id":"themes/next/layout/_partials/sidebar/site-overview.swig","hash":"c46849e0af8f8fb78baccd40d2af14df04a074af","modified":1748870133865},{"_id":"themes/next/layout/_scripts/pages/schedule.swig","hash":"077b5d66f6309f2e7dcf08645058ff2e03143e6c","modified":1748870133865},{"_id":"themes/next/layout/_partials/post/post-reward.swig","hash":"2b1a73556595c37951e39574df5a3f20b2edeaef","modified":1748870133864},{"_id":"themes/next/layout/_scripts/schemes/gemini.swig","hash":"1c910fc066c06d5fbbe9f2b0c47447539e029af7","modified":1748870133865},{"_id":"themes/next/layout/_scripts/schemes/mist.swig","hash":"7f14ef43d9e82bc1efc204c5adf0b1dbfc919a9f","modified":1748870133865},{"_id":"themes/next/layout/_scripts/schemes/muse.swig","hash":"7f14ef43d9e82bc1efc204c5adf0b1dbfc919a9f","modified":1748870133865},{"_id":"themes/next/layout/_scripts/schemes/pisces.swig","hash":"1c910fc066c06d5fbbe9f2b0c47447539e029af7","modified":1748870133865},{"_id":"themes/next/layout/_third-party/analytics/growingio.swig","hash":"5adea065641e8c55994dd2328ddae53215604928","modified":1748870133866},{"_id":"themes/next/layout/_third-party/chat/chatra.swig","hash":"f910618292c63871ca2e6c6e66c491f344fa7b1f","modified":1748870133866},{"_id":"themes/next/layout/_third-party/analytics/baidu-analytics.swig","hash":"4790058691b7d36cf6d2d6b4e93795a7b8d608ad","modified":1748870133866},{"_id":"themes/next/layout/_third-party/chat/tidio.swig","hash":"cba0e6e0fad08568a9e74ba9a5bee5341cfc04c1","modified":1748870133866},{"_id":"themes/next/layout/_third-party/analytics/google-analytics.swig","hash":"2fa2b51d56bfac6a1ea76d651c93b9c20b01c09b","modified":1748870133866},{"_id":"themes/next/layout/_third-party/analytics/index.swig","hash":"1472cabb0181f60a6a0b7fec8899a4d03dfb2040","modified":1748870133866},{"_id":"themes/next/layout/_third-party/comments/changyan.swig","hash":"f39a5bf3ce9ee9adad282501235e0c588e4356ec","modified":1748870133866},{"_id":"themes/next/layout/_third-party/comments/disqus.swig","hash":"b14908644225d78c864cd0a9b60c52407de56183","modified":1748870133866},{"_id":"themes/next/layout/_third-party/comments/disqusjs.swig","hash":"82f5b6822aa5ec958aa987b101ef860494c6cf1f","modified":1748870133866},{"_id":"themes/next/layout/_third-party/comments/gitalk.swig","hash":"d6ceb70648555338a80ae5724b778c8c58d7060d","modified":1748870133866},{"_id":"themes/next/layout/_third-party/math/index.swig","hash":"6c5976621efd5db5f7c4c6b4f11bc79d6554885f","modified":1748870133866},{"_id":"themes/next/layout/_third-party/comments/livere.swig","hash":"f7a9eca599a682479e8ca863db59be7c9c7508c8","modified":1748870133866},{"_id":"themes/next/layout/_third-party/comments/valine.swig","hash":"be0a8eccf1f6dc21154af297fc79555343031277","modified":1748870133866},{"_id":"themes/next/layout/_third-party/math/katex.swig","hash":"4791c977a730f29c846efcf6c9c15131b9400ead","modified":1748870133867},{"_id":"themes/next/layout/_third-party/math/mathjax.swig","hash":"ecf751321e799f0fb3bf94d049e535130e2547aa","modified":1748870133867},{"_id":"themes/next/layout/_third-party/search/localsearch.swig","hash":"767b6c714c22588bcd26ba70b0fc19b6810cbacd","modified":1748870133867},{"_id":"themes/next/layout/_third-party/statistics/busuanzi-counter.swig","hash":"4b1986e43d6abce13450d2b41a736dd6a5620a10","modified":1748870133867},{"_id":"themes/next/layout/_third-party/statistics/cnzz-analytics.swig","hash":"a17ace37876822327a2f9306a472974442c9005d","modified":1748870133867},{"_id":"themes/next/layout/_third-party/statistics/firestore.swig","hash":"b26ac2bfbe91dd88267f8b96aee6bb222b265b7a","modified":1748870133867},{"_id":"themes/next/layout/_third-party/search/algolia-search.swig","hash":"d35a999d67f4c302f76fdf13744ceef3c6506481","modified":1748870133867},{"_id":"themes/next/layout/_third-party/statistics/index.swig","hash":"5f6a966c509680dbfa70433f9d658cee59c304d7","modified":1748870133867},{"_id":"themes/next/layout/_third-party/search/swiftype.swig","hash":"ba0dbc06b9d244073a1c681ff7a722dcbf920b51","modified":1748870133867},{"_id":"themes/next/layout/_third-party/statistics/lean-analytics.swig","hash":"d56d5af427cdfecc33a0f62ee62c056b4e33d095","modified":1748870133867},{"_id":"themes/next/scripts/events/lib/config.js","hash":"d34c6040b13649714939f59be5175e137de65ede","modified":1748870133868},{"_id":"themes/next/layout/_third-party/tags/mermaid.swig","hash":"f3c43664a071ff3c0b28bd7e59b5523446829576","modified":1748870133867},{"_id":"themes/next/scripts/events/lib/injects.js","hash":"f233d8d0103ae7f9b861344aa65c1a3c1de8a845","modified":1748870133868},{"_id":"themes/next/scripts/filters/comment/changyan.js","hash":"a54708fd9309b4357c423a3730eb67f395344a5e","modified":1748870133868},{"_id":"themes/next/scripts/events/lib/injects-point.js","hash":"6661c1c91c7cbdefc6a5e6a034b443b8811235a1","modified":1748870133868},{"_id":"themes/next/scripts/filters/comment/default-config.js","hash":"7f2d93af012c1e14b8596fecbfc7febb43d9b7f5","modified":1748870133868},{"_id":"themes/next/scripts/filters/comment/common.js","hash":"2486f3e0150c753e5f3af1a3665d074704b8ee2c","modified":1748870133868},{"_id":"themes/next/scripts/filters/comment/disqus.js","hash":"4c0c99c7e0f00849003dfce02a131104fb671137","modified":1748870133868},{"_id":"themes/next/layout/_third-party/tags/pdf.swig","hash":"d30b0e255a8092043bac46441243f943ed6fb09b","modified":1748870133867},{"_id":"themes/next/scripts/filters/comment/livere.js","hash":"d5fefc31fba4ab0188305b1af1feb61da49fdeb0","modified":1748870133869},{"_id":"themes/next/scripts/filters/comment/gitalk.js","hash":"e51dc3072c1ba0ea3008f09ecae8b46242ec6021","modified":1748870133869},{"_id":"themes/next/scripts/filters/comment/disqusjs.js","hash":"7f8b92913d21070b489457fa5ed996d2a55f2c32","modified":1748870133868},{"_id":"themes/next/source/css/_variables/Gemini.styl","hash":"f4e694e5db81e57442c7e34505a416d818b3044a","modified":1748870133875},{"_id":"themes/next/source/css/_variables/Mist.styl","hash":"f70be8e229da7e1715c11dd0e975a2e71e453ac8","modified":1748870133875},{"_id":"themes/next/source/css/_variables/Pisces.styl","hash":"612ec843372dae709acb17112c1145a53450cc59","modified":1748870133875},{"_id":"themes/next/scripts/filters/comment/valine.js","hash":"6cbd85f9433c06bae22225ccf75ac55e04f2d106","modified":1748870133869},{"_id":"themes/next/source/css/_variables/Muse.styl","hash":"62df49459d552bbf73841753da8011a1f5e875c8","modified":1748870133875},{"_id":"themes/next/source/css/_variables/base.styl","hash":"818508748b7a62e02035e87fe58e75b603ed56dc","modified":1748870133875},{"_id":"themes/next/source/js/schemes/pisces.js","hash":"0ac5ce155bc58c972fe21c4c447f85e6f8755c62","modified":1748870133876},{"_id":"themes/next/source/lib/velocity/velocity.ui.min.js","hash":"ed5e534cd680a25d8d14429af824f38a2c7d9908","modified":1748870133877},{"_id":"themes/next/source/css/_common/components/back-to-top-sidebar.styl","hash":"ca5e70662dcfb261c25191cc5db5084dcf661c76","modified":1748870133870},{"_id":"themes/next/source/js/schemes/muse.js","hash":"1eb9b88103ddcf8827b1a7cbc56471a9c5592d53","modified":1748870133876},{"_id":"themes/next/source/css/_common/components/back-to-top.styl","hash":"a47725574e1bee3bc3b63b0ff2039cc982b17eff","modified":1748870133870},{"_id":"themes/next/source/css/_common/components/components.styl","hash":"8e7b57a72e757cf95278239641726bb2d5b869d1","modified":1748870133870},{"_id":"themes/next/source/css/_common/outline/mobile.styl","hash":"681d33e3bc85bdca407d93b134c089264837378c","modified":1748870133872},{"_id":"themes/next/source/lib/velocity/velocity.min.js","hash":"2f1afadc12e4cf59ef3b405308d21baa97e739c6","modified":1748870133877},{"_id":"themes/next/source/css/_common/outline/outline.styl","hash":"a1690e035b505d28bdef2b4424c13fc6312ab049","modified":1748870133872},{"_id":"themes/next/source/css/_common/components/reading-progress.styl","hash":"2e3bf7baf383c9073ec5e67f157d3cb3823c0957","modified":1748870133871},{"_id":"themes/next/source/css/_common/scaffolding/buttons.styl","hash":"a2e9e00962e43e98ec2614d6d248ef1773bb9b78","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/base.styl","hash":"0b2c4b78eead410020d7c4ded59c75592a648df8","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/normalize.styl","hash":"b56367ea676ea8e8783ea89cd4ab150c7da7a060","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/pagination.styl","hash":"8f58570a1bbc34c4989a47a1b7d42a8030f38b06","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/comments.styl","hash":"b1f0fab7344a20ed6748b04065b141ad423cf4d9","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/scaffolding.styl","hash":"523fb7b653b87ae37fc91fc8813e4ffad87b0d7e","modified":1748870133873},{"_id":"themes/next/source/css/_schemes/Gemini/index.styl","hash":"7785bd756e0c4acede3a47fec1ed7b55988385a5","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Mist/_header.styl","hash":"f6516d0f7d89dc7b6c6e143a5af54b926f585d82","modified":1748870133874},{"_id":"themes/next/source/css/_common/scaffolding/toggles.styl","hash":"179e33b8ac7f4d8a8e76736a7e4f965fe9ab8b42","modified":1748870133874},{"_id":"themes/next/source/css/_common/scaffolding/tables.styl","hash":"18ce72d90459c9aa66910ac64eae115f2dde3767","modified":1748870133873},{"_id":"themes/next/source/css/_schemes/Mist/_menu.styl","hash":"7104b9cef90ca3b140d7a7afcf15540a250218fc","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Mist/_layout.styl","hash":"bb7ace23345364eb14983e860a7172e1683a4c94","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Mist/_posts-expand.styl","hash":"6136da4bbb7e70cec99f5c7ae8c7e74f5e7c261a","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Mist/index.styl","hash":"a717969829fa6ef88225095737df3f8ee86c286b","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Muse/_header.styl","hash":"f0131db6275ceaecae7e1a6a3798b8f89f6c850d","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Muse/_layout.styl","hash":"4d1c17345d2d39ef7698f7acf82dfc0f59308c34","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Muse/_sub-menu.styl","hash":"c48ccd8d6651fe1a01faff8f01179456d39ba9b1","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Muse/index.styl","hash":"6ad168288b213cec357e9b5a97674ff2ef3a910c","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Muse/_menu.styl","hash":"93db5dafe9294542a6b5f647643cb9deaced8e06","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Muse/_sidebar.styl","hash":"2b2e7b5cea7783c9c8bb92655e26a67c266886f0","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Pisces/_layout.styl","hash":"70a4324b70501132855b5e59029acfc5d3da1ebd","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Pisces/_header.styl","hash":"e282df938bd029f391c466168d0e68389978f120","modified":1748870133874},{"_id":"themes/next/source/css/_schemes/Pisces/_sidebar.styl","hash":"44f47c88c06d89d06f220f102649057118715828","modified":1748870133875},{"_id":"themes/next/source/css/_schemes/Pisces/_menu.styl","hash":"85da2f3006f4bef9a2199416ecfab4d288f848c4","modified":1748870133875},{"_id":"themes/next/source/css/_schemes/Pisces/index.styl","hash":"6ad168288b213cec357e9b5a97674ff2ef3a910c","modified":1748870133875},{"_id":"themes/next/source/lib/font-awesome/webfonts/fa-regular-400.woff2","hash":"260bb01acd44d88dcb7f501a238ab968f86bef9e","modified":1748870133877},{"_id":"themes/next/source/css/_schemes/Pisces/_sub-menu.styl","hash":"e740deadcfc4f29c5cb01e40f9df6277262ba4e3","modified":1748870133875},{"_id":"themes/next/source/css/_common/components/pages/breadcrumb.styl","hash":"fafc96c86926b22afba8bb9418c05e6afbc05a57","modified":1748870133870},{"_id":"themes/next/source/css/_common/components/pages/pages.styl","hash":"7504dbc5c70262b048143b2c37d2b5aa2809afa2","modified":1748870133870},{"_id":"themes/next/source/lib/font-awesome/css/all.min.css","hash":"0038dc97c79451578b7bd48af60ba62282b4082b","modified":1748870133876},{"_id":"themes/next/source/css/_common/components/pages/categories.styl","hash":"2bd0eb1512415325653b26d62a4463e6de83c5ac","modified":1748870133870},{"_id":"themes/next/source/css/_common/components/pages/tag-cloud.styl","hash":"d21d4ac1982c13d02f125a67c065412085a92ff2","modified":1748870133870},{"_id":"themes/next/source/css/_common/components/post/post-collapse.styl","hash":"e75693f33dbc92afc55489438267869ae2f3db54","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-copyright.styl","hash":"f49ca072b5a800f735e8f01fc3518f885951dd8e","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/pages/schedule.styl","hash":"e771dcb0b4673e063c0f3e2d73e7336ac05bcd57","modified":1748870133870},{"_id":"themes/next/source/css/_common/components/post/post-eof.styl","hash":"902569a9dea90548bec21a823dd3efd94ff7c133","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-followme.styl","hash":"1e4190c10c9e0c9ce92653b0dbcec21754b0b69d","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-header.styl","hash":"65cb6edb69e94e70e3291e9132408361148d41d5","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-reward.styl","hash":"d114b2a531129e739a27ba6271cfe6857aa9a865","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-expand.styl","hash":"ded41fd9d20a5e8db66aaff7cc50f105f5ef2952","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-nav.styl","hash":"6a97bcfa635d637dc59005be3b931109e0d1ead5","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-gallery.styl","hash":"72d495a88f7d6515af425c12cbc67308a57d88ea","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-rtl.styl","hash":"f5c2788a78790aca1a2f37f7149d6058afb539e0","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-tags.styl","hash":"99e12c9ce3d14d4837e3d3f12fc867ba9c565317","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/post/post-widgets.styl","hash":"5b5649b9749e3fd8b63aef22ceeece0a6e1df605","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/third-party/gitalk.styl","hash":"8a7fc03a568b95be8d3337195e38bc7ec5ba2b23","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/third-party/math.styl","hash":"b49e9fbd3c182b8fc066b8c2caf248e3eb748619","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/third-party/related-posts.styl","hash":"e2992846b39bf3857b5104675af02ba73e72eed5","modified":1748870133871},{"_id":"themes/next/source/css/_common/outline/footer/footer.styl","hash":"454a4aebfabb4469b92a8cbb49f46c49ac9bf165","modified":1748870133872},{"_id":"themes/next/source/css/_common/components/post/post.styl","hash":"a760ee83ba6216871a9f14c5e56dc9bd0d9e2103","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/third-party/search.styl","hash":"9f0b93d109c9aec79450c8a0cf4a4eab717d674d","modified":1748870133871},{"_id":"themes/next/source/css/_common/components/third-party/third-party.styl","hash":"9a878d0119785a2316f42aebcceaa05a120b9a7a","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/github-banner.styl","hash":"e7a9fdb6478b8674b1cdf94de4f8052843fb71d9","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/header.styl","hash":"a793cfff86ad4af818faef04c18013077873f8f0","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/headerband.styl","hash":"0caf32492692ba8e854da43697a2ec8a41612194","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/menu.styl","hash":"5f432a6ed9ca80a413c68b00e93d4a411abf280a","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/bookmark.styl","hash":"e2d606f1ac343e9be4f15dbbaf3464bc4df8bf81","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/site-meta.styl","hash":"45a239edca44acecf971d99b04f30a1aafbf6906","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-blogroll.styl","hash":"44487d9ab290dc97871fa8dd4487016deb56e123","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/header/site-nav.styl","hash":"b2fc519828fe89a1f8f03ff7b809ad68cd46f3d7","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-dimmer.styl","hash":"9b479c2f9a9bfed77885e5093b8245cc5d768ec7","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-author-links.styl","hash":"2cb1876e9e0c9ac32160888af27b1178dbcb0616","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-author.styl","hash":"fa0222197b5eee47e18ac864cdc6eac75678b8fe","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-button.styl","hash":"1f0e7fbe80956f47087c2458ea880acf7a83078b","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-nav.styl","hash":"a960a2dd587b15d3b3fe1b59525d6fa971c6a6ec","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-toggle.styl","hash":"b3220db827e1adbca7880c2bb23e78fa7cbe95cb","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar-toc.styl","hash":"a05a4031e799bc864a4536f9ef61fe643cd421af","modified":1748870133872},{"_id":"themes/next/source/css/_common/outline/sidebar/site-state.styl","hash":"2a47f8a6bb589c2fb635e6c1e4a2563c7f63c407","modified":1748870133873},{"_id":"themes/next/source/css/_common/outline/sidebar/sidebar.styl","hash":"a9cd93c36bae5af9223e7804963096274e8a4f03","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/highlight/copy-code.styl","hash":"f71a3e86c05ea668b008cf05a81f67d92b6d65e4","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/highlight/diff.styl","hash":"d3f73688bb7423e3ab0de1efdf6db46db5e34f80","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/highlight/highlight.styl","hash":"35c871a809afa8306c8cde13651010e282548bc6","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/highlight/theme.styl","hash":"3b3acc5caa0b95a2598bef4eeacb21bab21bea56","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/tags/blockquote-center.styl","hash":"1d2778ca5aeeeafaa690dc2766b01b352ab76a02","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/tags/group-pictures.styl","hash":"709d10f763e357e1472d6471f8be384ec9e2d983","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/tags/label.styl","hash":"d7fce4b51b5f4b7c31d93a9edb6c6ce740aa0d6b","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/tags/note.styl","hash":"e4d9a77ffe98e851c1202676940097ba28253313","modified":1748870133873},{"_id":"themes/next/source/css/_common/scaffolding/tags/pdf.styl","hash":"b49c64f8e9a6ca1c45c0ba98febf1974fdd03616","modified":1748870133874},{"_id":"themes/next/source/css/_common/scaffolding/tags/tabs.styl","hash":"f23670f1d8e749f3e83766d446790d8fd9620278","modified":1748870133874},{"_id":"themes/next/source/css/_common/scaffolding/tags/tags.styl","hash":"9e4c0653cfd3cc6908fa0d97581bcf80861fb1e7","modified":1748870133874},{"_id":"source/imgs/clion_engine.png","hash":"134315122252e6ba82ca2f868d80b6acf6c321a0","modified":1748870133855},{"_id":"themes/next/source/lib/font-awesome/webfonts/fa-brands-400.woff2","hash":"509988477da79c146cb93fb728405f18e923c2de","modified":1748870133877},{"_id":"themes/next/source/lib/font-awesome/webfonts/fa-solid-900.woff2","hash":"75a88815c47a249eadb5f0edc1675957f860cca7","modified":1748870133877},{"_id":"public/categories/index.html","hash":"e99e4eac8da6294af94ec006a836c9b74721ec51","modified":1748870149484},{"_id":"public/about/index.html","hash":"41c7f3e53a9726bb769644391b770f8342fe0ffa","modified":1748870149484},{"_id":"public/tags/index.html","hash":"fd5220dc8832a252333e5bb8f754f113112fdc43","modified":1748870149484},{"_id":"public/2019/10/17/小功能之回退首页可行性解决方案探索/index.html","hash":"dea01a765fd5bcaea788356ba031ea36cc091c21","modified":1748870149484},{"_id":"public/archives/index.html","hash":"227b1bfc166ff48243ccae73b78c730a7aae4632","modified":1748870149484},{"_id":"public/archives/page/2/index.html","hash":"b4ba9e0995464f1f6bbd041b7f7f8dab4b23085f","modified":1748870149484},{"_id":"public/archives/2019/index.html","hash":"b3f06b330f50f249db4688f4338d00aa8e800c95","modified":1748870149484},{"_id":"public/archives/2019/05/index.html","hash":"0d93390f36f4a4586a9f649ffeb2fd8f0de2365c","modified":1748870149484},{"_id":"public/archives/2019/06/index.html","hash":"e10a65205960ca201550842db0c6b2ea776ad824","modified":1748870149484},{"_id":"public/archives/2019/07/index.html","hash":"713d12b85b43ba53b04ff47cf7c7ddf0a8255f02","modified":1748870149484},{"_id":"public/archives/2019/08/index.html","hash":"3cd276b0050fcabd01c2741fe73c6d46971092cb","modified":1748870149484},{"_id":"public/archives/2019/09/index.html","hash":"19956af14793b74418192fbb21fd15955a809bfd","modified":1748870149484},{"_id":"public/archives/2019/10/index.html","hash":"c30ba54c481d44db3a1d009c3640a15e0ca19ace","modified":1748870149484},{"_id":"public/archives/2019/11/index.html","hash":"d43aa157f4c3de733e7d32515be4fc39d5c93c59","modified":1748870149484},{"_id":"public/archives/2019/12/index.html","hash":"2b3f6f12b464caff053d712979a2d88c19e95878","modified":1748870149484},{"_id":"public/archives/2020/index.html","hash":"263cb8a0f8de961a9226df29b7cefff62a702a7f","modified":1748870149484},{"_id":"public/archives/2020/03/index.html","hash":"d9610df7326e37490c8e2d74d474df93c452425b","modified":1748870149484},{"_id":"public/archives/2020/05/index.html","hash":"08724cc0c6d372ae4a89c026f2c2a8b80d0f835b","modified":1748870149484},{"_id":"public/archives/2025/index.html","hash":"40961a8c8b1832cad21c61fe2d38c1f133c7fd9f","modified":1748870149484},{"_id":"public/archives/2025/05/index.html","hash":"2907c2bc9de744379cbb59e3f1d06a34ebd89279","modified":1748870149484},{"_id":"public/archives/2025/06/index.html","hash":"d0b83af765ed925582348ab65a5b0a0054659c20","modified":1748870149484},{"_id":"public/page/2/index.html","hash":"bfd84fdad94c41d28f01882925c6f4697d027491","modified":1748870149484},{"_id":"public/categories/Android/index.html","hash":"b17cef43fd2c60dbc623d7f451de48bf1477da2b","modified":1748870149484},{"_id":"public/categories/Flutter/index.html","hash":"8bdcb189a354da53054773c859d7ecb98de63568","modified":1748870149484},{"_id":"public/tags/渲染/index.html","hash":"a0bce99b3d4c2548f6944896362b7e2a9070c353","modified":1748870149484},{"_id":"public/tags/rust/index.html","hash":"68f86153246754bb96211c518679bc8ed1cfaf8c","modified":1748870149484},{"_id":"public/tags/小功能组/index.html","hash":"d9811ec91815115743c99c9612556a8955344722","modified":1748870149484},{"_id":"public/tags/埋点系统/index.html","hash":"7c49d7f598ecd133d19da84ec44216159fd81b09","modified":1748870149484},{"_id":"public/tags/插件化/index.html","hash":"6cf079ff9f87970a7be9a83fee83458e7709b598","modified":1748870149484},{"_id":"public/tags/应用性能/index.html","hash":"3b5f3e5762a2e40ae0207f9f417931ebfe1cfb05","modified":1748870149484},{"_id":"public/2025/06/02/rokio源码实现简单分析/index.html","hash":"0a00c74fda16868e69cbd1ab6b5b5d18093e9f5f","modified":1748870149484},{"_id":"public/2025/05/03/Android渲染之Surface与ANativeWindow/index.html","hash":"649bec4bb25e56b53781f890f555cc8ee84d6273","modified":1748870149484},{"_id":"public/2020/05/02/快速开闭闪光灯实现与风险规避/index.html","hash":"babf1390d1686f8233cad929b52d5e89a4df8c61","modified":1748870149484},{"_id":"public/2020/03/27/MacOS环境下Flutter Engine编译纪要/index.html","hash":"39ca4b4e5a6547b2f12dd97b43ca086689e33b7b","modified":1748870149484},{"_id":"public/2019/12/12/介绍两款androidX迁移利器/index.html","hash":"1492e73e81d9f46d1c392dea58a99664857636cd","modified":1748870149484},{"_id":"public/2019/11/16/一种有效管控APP隐私权限的解决方案/index.html","hash":"ff96943974ea6ad69a4a75abdd18fd7292a1b78a","modified":1748870149484},{"_id":"public/2019/09/20/治理令人头痛的pthread-create-OutOfMemoryError错误/index.html","hash":"e6a089cf371fa5650d3fdbfcfba32c042c2ab6fc","modified":1748870149484},{"_id":"public/2019/08/03/埋点系统之可信数据持续集成实践/index.html","hash":"c792d0c4df863b766aa73aeb37214d9c6617767a","modified":1748870149484},{"_id":"public/2019/07/19/插件化之AAPT客户化/index.html","hash":"453785439946e8056ebca7cb408d519e1af4cc79","modified":1748870149484},{"_id":"public/2019/06/29/插件化之启动优化实践/index.html","hash":"d9e5fd7a15dbc17609f342eed189c21266eedbf5","modified":1748870149484},{"_id":"public/2019/05/23/插件化之插件混淆的可行性探索/index.html","hash":"ed435ccf160d0846617fdef2f31fd62a049d8ccd","modified":1748870149484},{"_id":"public/index.html","hash":"c7236ffa334c4a879adb996db48a1d9bf8dca596","modified":1748870149484},{"_id":"public/CNAME","hash":"da39a3ee5e6b4b0d3255bfef95601890afd80709","modified":1748870149484},{"_id":"public/imgs/appstart2.png","hash":"148765204232192f72085ccb14add09f40c2c1d4","modified":1748870149484},{"_id":"public/imgs/appstart.png","hash":"6a61ff23be62139e69021c07e1e9ecf7e28ae5a4","modified":1748870149484},{"_id":"public/images/algolia_logo.svg","hash":"ec119560b382b2624e00144ae01c137186e91621","modified":1748870149484},{"_id":"public/images/apple-touch-icon-next.png","hash":"2959dbc97f31c80283e67104fe0854e2369e40aa","modified":1748870149484},{"_id":"public/images/avatar.gif","hash":"18c53e15eb0c84b139995f9334ed8522b40aeaf6","modified":1748870149484},{"_id":"public/images/cc-by-nc.svg","hash":"8d39b39d88f8501c0d27f8df9aae47136ebc59b7","modified":1748870149484},{"_id":"public/images/cc-by-nc-sa.svg","hash":"3031be41e8753c70508aa88e84ed8f4f653f157e","modified":1748870149484},{"_id":"public/images/cc-by-nd.svg","hash":"c563508ce9ced1e66948024ba1153400ac0e0621","modified":1748870149484},{"_id":"public/images/cc-by-nc-nd.svg","hash":"c6524ece3f8039a5f612feaf865d21ec8a794564","modified":1748870149484},{"_id":"public/images/cc-by-sa.svg","hash":"aa4742d733c8af8d38d4c183b8adbdcab045872e","modified":1748870149484},{"_id":"public/images/cc-by.svg","hash":"28a0a4fe355a974a5e42f68031652b76798d4f7e","modified":1748870149484},{"_id":"public/images/favicon-16x16-next.png","hash":"943a0d67a9cdf8c198109b28f9dbd42f761d11c3","modified":1748870149484},{"_id":"public/images/favicon-32x32-next.png","hash":"0749d7b24b0d2fae1c8eb7f671ad4646ee1894b1","modified":1748870149484},{"_id":"public/images/cc-zero.svg","hash":"87669bf8ac268a91d027a0a4802c92a1473e9030","modified":1748870149484},{"_id":"public/lib/font-awesome/webfonts/fa-regular-400.woff2","hash":"260bb01acd44d88dcb7f501a238ab968f86bef9e","modified":1748870149484},{"_id":"public/images/logo.svg","hash":"d29cacbae1bdc4bbccb542107ee0524fe55ad6de","modified":1748870149484},{"_id":"public/imgs/WechatIMG2.jpeg","hash":"765ef9e59c74f0a19d7e132b347d8dec10cdebcd","modified":1748870149484},{"_id":"public/imgs/b6f8dc13.png","hash":"9f81560769ad7efb65dd08b4cd8d3dc9461fe3a9","modified":1748870149484},{"_id":"public/imgs/filter.png","hash":"c88572ae58d4e532e408ec11ee1a44078fc7d6c5","modified":1748870149484},{"_id":"public/imgs/splash.png","hash":"0dc286274b8b99e34a2cf48a57ff8d1d70f7f68e","modified":1748870149484},{"_id":"public/js/algolia-search.js","hash":"498d233eb5c7af6940baf94c1a1c36fdf1dd2636","modified":1748870149484},{"_id":"public/js/bookmark.js","hash":"9734ebcb9b83489686f5c2da67dc9e6157e988ad","modified":1748870149484},{"_id":"public/js/local-search.js","hash":"35ccf100d8f9c0fd6bfbb7fa88c2a76c42a69110","modified":1748870149484},{"_id":"public/js/motion.js","hash":"72df86f6dfa29cce22abeff9d814c9dddfcf13a9","modified":1748870149484},{"_id":"public/js/next-boot.js","hash":"a1b0636423009d4a4e4cea97bcbf1842bfab582c","modified":1748870149484},{"_id":"public/js/utils.js","hash":"730cca7f164eaf258661a61ff3f769851ff1e5da","modified":1748870149484},{"_id":"public/js/schemes/muse.js","hash":"1eb9b88103ddcf8827b1a7cbc56471a9c5592d53","modified":1748870149484},{"_id":"public/js/schemes/pisces.js","hash":"0ac5ce155bc58c972fe21c4c447f85e6f8755c62","modified":1748870149484},{"_id":"public/lib/velocity/velocity.ui.min.js","hash":"ed5e534cd680a25d8d14429af824f38a2c7d9908","modified":1748870149484},{"_id":"public/css/main.css","hash":"5815fad0a6adbba7fd6912fded2d5acfcd8e71cf","modified":1748870149484},{"_id":"public/lib/anime.min.js","hash":"47cb482a8a488620a793d50ba8f6752324b46af3","modified":1748870149484},{"_id":"public/lib/velocity/velocity.min.js","hash":"2f1afadc12e4cf59ef3b405308d21baa97e739c6","modified":1748870149484},{"_id":"public/lib/font-awesome/css/all.min.css","hash":"0038dc97c79451578b7bd48af60ba62282b4082b","modified":1748870149484},{"_id":"public/lib/font-awesome/webfonts/fa-brands-400.woff2","hash":"509988477da79c146cb93fb728405f18e923c2de","modified":1748870149484},{"_id":"public/imgs/android2.png","hash":"9ba7e6c2aed48890e0d792c436a9223b301fcff5","modified":1748870149484},{"_id":"public/lib/font-awesome/webfonts/fa-solid-900.woff2","hash":"75a88815c47a249eadb5f0edc1675957f860cca7","modified":1748870149484},{"_id":"public/imgs/state.png","hash":"818540e78ae0927098803b7d54fb9060e3e6212a","modified":1748870149484},{"_id":"public/imgs/android3.png","hash":"c71843dd8e529d3247bf8da9d7eacd8258209130","modified":1748870149484},{"_id":"public/imgs/tokio_classes.png","hash":"f5556753c15c5f9ad766befc53593f01c1705a72","modified":1748870149484},{"_id":"public/imgs/29f3ac36.png","hash":"a61f916823905241ad3b5d5f0593f3396170c504","modified":1748870149484},{"_id":"public/imgs/android1.png","hash":"59e048b3889a79f6d22a73de904f31a18e87ebe6","modified":1748870149484},{"_id":"public/imgs/clion_engine.png","hash":"134315122252e6ba82ca2f868d80b6acf6c321a0","modified":1748870149484}],"Category":[{"name":"Android","_id":"cmbf44n860004cate7x3c4vaw"},{"name":"Flutter","_id":"cmbf44n88000acate2577gtjh"}],"Data":[],"Page":[{"title":"categories","date":"2019-05-23T10:56:27.000Z","type":"categories","comments":0,"_content":"","source":"categories/index.md","raw":"---\ntitle: categories\ndate: 2019-05-23 18:56:27\ntype: \"categories\"\ncomments: false\n---\n","updated":"2025-06-02T13:15:33.846Z","path":"categories/index.html","layout":"page","_id":"cmbf44n820000cate4hcp0qfh","content":"","excerpt":"","more":""},{"title":"about me","date":"2019-06-30T04:38:20.000Z","_content":"# 关于我\n2013年毕业,先后就职于创业软件、微店、饿了么和小米,主要主导插件化、数据采集系统和小程序容器开发,现就职于饿了么,负责饿了么和口碑APP架构相关工作。19年开始,逼迫自已写一些技术blog。\n\n# 联系我\n\n微信:\n \n\n\n","source":"about/index.md","raw":"---\ntitle: about me\ndate: 2019-06-30 12:38:20\n---\n# 关于我\n2013年毕业,先后就职于创业软件、微店、饿了么和小米,主要主导插件化、数据采集系统和小程序容器开发,现就职于饿了么,负责饿了么和口碑APP架构相关工作。19年开始,逼迫自已写一些技术blog。\n\n# 联系我\n\n微信:\n \n\n\n","updated":"2025-06-02T13:15:33.846Z","path":"about/index.html","comments":1,"layout":"page","_id":"cmbf44n850002cate075q964e","content":"<h1 id=\"关于我\"><a href=\"#关于我\" class=\"headerlink\" title=\"关于我\"></a>关于我</h1><p>2013年毕业,先后就职于创业软件、微店、饿了么和小米,主要主导插件化、数据采集系统和小程序容器开发,现就职于饿了么,负责饿了么和口碑APP架构相关工作。19年开始,逼迫自已写一些技术blog。</p>\n<h1 id=\"联系我\"><a href=\"#联系我\" class=\"headerlink\" title=\"联系我\"></a>联系我</h1><p>微信:<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/WechatIMG2.jpeg\"> </p>\n","excerpt":"","more":"<h1 id=\"关于我\"><a href=\"#关于我\" class=\"headerlink\" title=\"关于我\"></a>关于我</h1><p>2013年毕业,先后就职于创业软件、微店、饿了么和小米,主要主导插件化、数据采集系统和小程序容器开发,现就职于饿了么,负责饿了么和口碑APP架构相关工作。19年开始,逼迫自已写一些技术blog。</p>\n<h1 id=\"联系我\"><a href=\"#联系我\" class=\"headerlink\" title=\"联系我\"></a>联系我</h1><p>微信:<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/WechatIMG2.jpeg\"> </p>\n"},{"title":"tags","date":"2019-05-23T10:55:06.000Z","type":"tags","comments":0,"_content":"","source":"tags/index.md","raw":"---\ntitle: tags\ndate: 2019-05-23 18:55:06\ntype: \"tags\"\ncomments: false\n---\n","updated":"2025-06-02T13:15:33.858Z","path":"tags/index.html","layout":"page","_id":"cmbf44n870006catecfjfecu9","content":"","excerpt":"","more":""}],"Post":[{"title":"Android独立窗口自绘渲染实现","date":"2025-05-03T11:19:19.000Z","_content":"# 引言\n在Android中,独立窗口自绘渲染,典型应用场景比如SurfaceView与手机双边侧滑返回动画面板(WMS.addView)。最近入职一家新公司,涉及对接更底层渲染实现,具体表现在NDK层,获取一个独立Window窗口,上层用Skia进行绘制,并在Android系统中渲染出来。本文旨在分析WMS.addView链路,明淅渲染关键路径,为后面自定义渲染作支持。\n<!-- more -->\n# 核心类说明\n\n在 Android 系统中,独立窗口自绘渲染,绕不开`Surface` 和 `ANativeWindow` ,二者是与图形渲染和窗口管理相关的核心类,它们的关系和功能可以总结如下:\n\n## **类结构解释**\n```cpp\nclass Surface : public ANativeObjectBase<ANativeWindow, Surface, RefBase>{\n\n}\n```\n- **继承关系**: \n `Surface` 继承自模板类 `ANativeObjectBase<ANativeWindow, Surface, RefBase>`。\n- **模板参数含义**:\n - **`ANativeWindow`**:表示底层的原生窗口接口。\n - **`Surface`**:子类自身(使用 CRTP 模式,允许基类调用子类方法)。\n - **`RefBase`**:Android 的引用计数基类,用于对象生命周期管理。\n\n---\n\n## **Surface 的作用**\n- **图形渲染的抽象层**: \n `Surface` 是 Android 应用与显示系统之间的桥梁,代表一个可绘制的表面。应用程序通过 `Surface` 进行 UI 渲染(例如通过 Canvas 或 OpenGL ES)。\n- **跨进程通信**: \n `Surface` 实现了 `Parcelable` 接口,可跨进程传递(如从应用进程传递到 SurfaceFlinger 服务进程)。\n- **缓冲区管理**: \n 管理图形缓冲区队列(`BufferQueue`),协调生产者和消费者(如应用和 SurfaceFlinger)之间的缓冲区交换。\n\n---\n\n## **ANativeWindow 的作用**\n- **原生窗口的 C 接口**: \n `ANativeWindow` 是 Android NDK 中定义的抽象,提供对底层窗口系统的访问(如通过 `ANativeWindow_fromSurface()` 获取)。\n- **跨平台兼容性**: \n 封装了不同硬件/平台的窗口操作(例如设置缓冲区大小、格式,提交渲染结果)。\n- **与 Surface 的关系**: \n `ANativeWindow` 是 `Surface` 的底层接口,`Surface` 类通过继承 `ANativeObjectBase` 实现了 `ANativeWindow` 的功能。\n\n---\n\n## **两者关系**\n1. **继承与封装**: \n `Surface` 是 `ANativeWindow` 的高层封装,提供更易用的 C++ API,而 `ANativeWindow` 是底层的 C 风格接口。\n2. **功能实现**: \n `Surface` 通过 `ANativeObjectBase` 模板类继承 `ANativeWindow` 的接口,并实现其方法(如 `dequeueBuffer`/`queueBuffer`),最终通过 `RefBase` 管理生命周期。\n3. **使用场景**:\n - **Java/Kotlin 层**:通过 `Surface` 类进行 UI 渲染。\n - **Native 层(NDK)**:通过 `ANativeWindow` 直接操作窗口(例如 Vulkan/OpenGL ES 渲染)。\n\n\n---\n\n## **总结**\n- **`ANativeWindow`**:底层原生窗口接口,提供跨平台的窗口操作(NDK 使用)。\n- **`Surface`**:高层封装,整合 `ANativeWindow` 功能并提供 Android 框架级的渲染管理。\n- **关系**:`Surface` 是 `ANativeWindow` 的面向对象实现,二者共同服务于图形渲染,前者用于 Java/C++ 框架层,后者用于更接近硬件的 NDK 层。\n\n# WMS.addView自绘链路\n\n在Android 10及以后,WMS.addView可以以侧滑返回动画面板实现代码作说明,代码主要在[systemui/navigationbar/gestural](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/),核心类EdgeBackGestureHandler、BackPanelController、BackPanel,这三个组件共同构成了 Android 的边缘返回手势系统,提供流畅的用户体验,是一个WMS.addView应用场景。\n其中ViewCaptureAwareWindowManager以一个独立的Window添加BackPanel(View),BackPanel响应相应滑动事件,用Canvas做自绘实现,具体链路包括:\n\n## **[BackPanelController#setLayoutParams](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java;drc=61197364367c9e404c7da6900658f1b16c42d0da;bpv=0;bpt=1;l=813)**\n\n```java\n//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler\noverride fun setLayoutParams(layoutParams: WindowManager.LayoutParams) {\n this.layoutParams = layoutParams\n windowManager.addView(mView, layoutParams)\n }\n```\n\nlayoutParams来自EdgeBackGestureHandler#createLayoutParams\n```java\n//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler\n private WindowManager.LayoutParams createLayoutParams() {\n Resources resources = mContext.getResources();\n //TYPE_NAVIGATION_BAR_PANEL: 表示这是一个导航栏面板窗口,优先级低于系统UI但高于普通应用\n //FLAG_NOT_FOCUSABLE: 窗口不能获得焦点\n //FLAG_NOT_TOUCHABLE: 窗口不接收触摸事件\n //FLAG_LAYOUT_IN_SCREEN: 窗口布局在屏幕坐标系中\n WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(\n resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_width),\n resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_height),\n WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,\n WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE\n | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,\n PixelFormat.TRANSLUCENT); //半透明\n layoutParams.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel);\n layoutParams.windowAnimations = 0;\n layoutParams.privateFlags |=\n (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS\n | PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION);\n layoutParams.setTitle(TAG + mContext.getDisplayId());\n layoutParams.setFitInsetsTypes(0 /* types */);\n layoutParams.setTrustedOverlay();\n return layoutParams;\n }\n```\n## **WindowManager.addView 绑定Window**\nWindowManager.addView与WMS处理Window绑定。\n1. **WindowManager.addView 调用链**:\n```java\n// frameworks/base/core/java/android/view/WindowManagerImpl.java\npublic void addView(View view, ViewGroup.LayoutParams params) {\n mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);\n}\n```\n\n2. **WindowManagerGlobal 中的处理**:\n```java\n// frameworks/base/core/java/android/view/WindowManagerGlobal.java\npublic void addView(View view, ViewGroup.LayoutParams params,\n Display display, Window parentWindow) {\n \n // 1. 创建 ViewRootImpl\n if (windowlessSession == null) {\n root = new ViewRootImpl(view.getContext(), display);\n } else {\n root = new ViewRootImpl(view.getContext(), display,\n windowlessSession, new WindowlessWindowLayout());\n }\n \n // 2. 保存引用\n mViews.add(view);\n mRoots.add(root);\n mParams.add(wparams);\n\n // 3. 设置参数\n try {\n root.setView(view, wparams, panelParentView, userId);\n } catch (RuntimeException e) {\n throw e;\n }\n}\n```\n\n3. **`ViewRootImpl.setView()` 触发窗口创建**\n在 `setView()` 中,通过 IPC 调用 `WindowManagerService` 创建窗口:\n```java\n// ViewRootImpl.java\npublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) {\n // ... 其他逻辑\n try {\n // 通过 mWindowSession (WindowManagerGlobal.getWindowSession())与 WMS 通信\n mWindowSession.addToDisplayAsUser(\n mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId,\n mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets, mControls);\n } catch (RemoteException e) {\n // 处理异常\n }\n}\n//frameworks/base/services/core/java/com/android/server/wm/Session.java\n @Override\n public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,\n int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes,\n InputChannel outInputChannel, InsetsState outInsetsState,\n InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,\n float[] outSizeCompatScale) {\n return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,\n requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,\n outAttachedFrame, outSizeCompatScale);\n }\n```\n- **`mWindowSession`** 是 `IWindowSession` 的实例,由 `WindowManagerGlobal` 创建,是 App 进程与 WMS 通信的代理。\n- **`addToDisplayAsUser`** 是 IPC 调用,通知 WMS 创建窗口并分配资源。\n\n在 WMS 服务端,`Session.addToDisplayAsUser()` 最终会创建 `WindowState`:\n```java\n// WindowManagerService.java\npublic int addWindow(Session session, IWindow client, ...) {\n // ... 权限校验、参数处理\n final WindowState win = new WindowState(this, session, client, token, parentWindow,\n appOp[0], seq, attrs, viewVisibility, session.mUid, userId);\n win.mSession.onWindowAdded(win);\n mWindowMap.put(client.asBinder(), win);\n}\n```\n- **`WindowState`** 是 WMS 中窗口的抽象,管理窗口的层级、可见性等。\n\n## **`ViewRootImpl.relayoutWindow` 绑定Surface**\n`SurfaceControl` 的创建实际发生在 **窗口的首次布局(`performTraversals`)阶段**,由 `WindowManagerService` 触发。关键流程如下:\n在客户端(应用进程)的 `ViewRootImpl` 中,通过 IPC 调用 `WindowManagerService` 的 `relayoutWindow` 方法,请求更新窗口布局并创建 `Surface`:\n```java\n// frameworks/base/core/java/android/view/ViewRootImpl.java\nprivate final WindowRelayoutResult mRelayoutResult = new WindowRelayoutResult(\n mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls);\nprivate int relayoutWindow(WindowManager.LayoutParams params, ...) throws RemoteException {\n // 调用 WMS 的 relayoutWindow 方法\n relayoutResult = mWindowSession.relayout(mWindow, params,\n requestedWidth, requestedHeight, viewVisibility,\n insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,\n mRelayoutSeq, mLastSyncSeqId, mRelayoutResult);\n if (mSurfaceControl.isValid()) {\n updateBlastSurfaceIfNeeded();\n }\n // ...\n}\n\n// Android 图形系统的底层优化,引入 BLAST (BufferQueue Layer State Traversal) 机制\nvoid updateBlastSurfaceIfNeeded() {\n mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,\n mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);\n mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);\n mBlastBufferQueue.setApplyToken(mBbqApplyToken);\n Surface blastSurface;\n if (addSchandleToVriSurface()) {\n blastSurface = mBlastBufferQueue.createSurfaceWithHandle();\n } else {\n blastSurface = mBlastBufferQueue.createSurface();\n }\n // 更新 Surface\n mSurface.transferFrom(blastSurface);\n}\n//createSurfaceWithHandle或createSurface调用nativeGetSurface\n// frameworks/base/libs/hwui/BLASTBufferQueue.cpp\n\n// JNI 转换\n//通过 nativePtr 获取 Native 层的 BLASTBufferQueue 实例。\n//调用 getSurface() 获取 sp<Surface>。\n//使用 android_view_Surface_createFromSurface 将 Native Surface 转换为 Java Surface 对象。\nstatic jobject BLASTBufferQueue_getSurface(JNIEnv* env, jclass clazz, jlong nativePtr) {\n BLASTBufferQueue* bbq = reinterpret_cast<BLASTBufferQueue*>(nativePtr);\n sp<Surface> surface = bbq->getSurface();\n return android_view_Surface_createFromSurface(env, surface);\n}\n\n// frameworks/native/libs/gui/BLASTBufferQueue.cpp\n//Surface 是 ANativeWindow 的子类,其底层通过 IGraphicBufferProducer(生产者接口)与 BufferQueue 绑定。在 BLASTBufferQueue 中,生产者接口 mProducer 被传递给 Surface,使其能够通过 dequeueBuffer 和 queueBuffer 管理图形缓冲区\n//Surface 内部持有 IGraphicBufferProducer\n//BBQSurface继承Surface\nsp<Surface> BLASTBufferQueue::getSurface(bool includeSurfaceControlHandle) {\n std::lock_guard _lock{mMutex};\n sp<IBinder> scHandle = nullptr;\n if (includeSurfaceControlHandle && mSurfaceControl) {\n scHandle = mSurfaceControl->getHandle();\n }\n return new BBQSurface(mProducer, true, scHandle, this);\n}\n```\n在服务端(`WindowManagerService`),`mWindowSession.relayout` 最终会调用 `WindowStateAnimator.createSurfaceLocked()` 创建 `SurfaceControl`:\n```java\n//frameworks/base/services/core/java/com/android/server/wm/Session.java\n @Override\npublic int relayout(IWindow window, WindowManager.LayoutParams attrs,\n int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq,\n int lastSyncSeqId, WindowRelayoutResult outRelayoutResult) {\n int res = mService.relayoutWindow(this, window, attrs, requestedWidth,\n requestedHeight, viewFlags, flags, seq, lastSyncSeqId, outRelayoutResult);\n return res;\n}\n// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java\npublic int relayoutWindow(Session session, IWindow client, LayoutParams attrs,\n int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq,\n int lastSyncSeqId, WindowRelayoutResult outRelayoutResult) {\n result = createSurfaceControl(outSurfaceControl, result, win, winAnimator); \n}\n\n// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java\nprivate int createSurfaceControl(SurfaceControl outSurfaceControl, int result,\n WindowState win, WindowStateAnimator winAnimator) {\n if (!win.mHasSurface) {\n result |= RELAYOUT_RES_SURFACE_CHANGED;\n }\n\n SurfaceControl surfaceControl;\n try {\n Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, \"createSurfaceControl\");\n surfaceControl = winAnimator.createSurfaceLocked();\n } finally {\n Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);\n }\n return result;\n}\n// frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java\nvoid createSurfaceLocked() {\n mSurfaceControl = mWin.makeSurface()\n .setParent(mWin.mSurfaceControl)\n .setName(mTitle)\n .setFormat(format)\n .setFlags(flags)\n .setMetadata(METADATA_WINDOW_TYPE, attrs.type)\n .setMetadata(METADATA_OWNER_UID, mSession.mUid)\n .setMetadata(METADATA_OWNER_PID, mSession.mPid)\n .setCallsite(\"WindowSurfaceController\")\n .setBLASTLayer().build();\n}\n```\n\n## **视图通过 `Surface` 绘制内容**\n在 `ViewRootImpl` 的绘制流程中,通过 `Surface` 提交帧数据:\n```java\n// ViewRootImpl.java -> performTraversals()\nprivate void performTraversals() {\n // 1. 测量、布局\n measureHierarchy(...);\n layout(...);\n // 2. 绘制到 Surface\n performDraw(...)\n}\n\nprivate boolean performDraw(@Nullable SurfaceSyncGroup surfaceSyncGroup) {\n usingAsyncReport = draw(fullRedrawNeeded, surfaceSyncGroup, mSyncBuffer);\n}\n\nprivate boolean draw(boolean fullRedrawNeeded, @Nullable SurfaceSyncGroup activeSyncGroup,\n boolean syncBuffer) {\n if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,\n scalingRequired, dirty, surfaceInsets)) {\n return false;\n }\n}\n\nprivate boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,\n boolean scalingRequired, Rect dirty, Rect surfaceInsets) {\n Surface surface = mSurface;\n Canvas canvas = surface.lockCanvas(dirty);\n // 通过 View 系统绘制内容\n mView.draw(canvas);\n surface.unlockCanvasAndPost(canvas);\n}\n```\n- **`surface.lockCanvas()`** 获取 `Canvas` 对象,用于绘制。\n- **`unlockCanvasAndPost()`** 提交绘制内容到 `Surface`,最终由 SurfaceFlinger 合成显示。\n\n## **总结流程**\n1. **`addView` 触发 `ViewRootImpl.setView()`** \n 客户端通过 IPC 调用 `WMS.addWindow()`,创建 `WindowState`。\n\n2. **首次布局请求** \n `ViewRootImpl.relayoutWindow()` 调用 `WMS.relayoutWindow()`,触发 `SurfaceControl` 的创建。\n\n3. **`SurfaceControl` 的创建** \n 服务端通过 `SurfaceSession` 创建 `SurfaceControl`,并通过 Binder 将句柄返回客户端。\n\n4. **客户端 `Surface` 绑定** \n 客户端 `ViewRootImpl` 通过 `updateBlastSurfaceIfNeeded` 将 `Surface` 与 `SurfaceControl` 绑定。\n\n5. **绘制提交** \n 客户端通过 `Surface.lockCanvas()` 和 `unlockCanvasAndPost()` 将内容绘制到 `Surface`,由 SurfaceFlinger 合成显示。\n\n---\n\n## **关键绑定关系总结**\n| **组件** | **作用** |\n|--------------------|-------------------------------------------------------------------------|\n| `ViewRootImpl` | 连接视图系统与 WMS,管理 `Surface` 生命周期和绘制流程。 |\n| `WindowState` | WMS 中的窗口抽象,持有 `SurfaceControl` 控制 Surface 属性。 |\n| `SurfaceControl` | 服务端(WMS)对 `Surface` 的控制句柄,管理缓冲区分配和属性,对应 SurfaceFlinger 的 Layer。 |\n| `Surface` | 客户端(应用进程)的绘制接口,通过 Binder 持有服务端 `SurfaceControl` 的引用。 |\n\n---\n\n## **流程总结**\n1. **窗口创建** \n `addView` 触发 `ViewRootImpl` 创建,并通过 IPC 调用 WMS 的 `addToDisplayAsUser`,在服务端生成 `WindowState` 和 `SurfaceControl`。\n\n2. **Surface 分配** \n WMS 通过 `SurfaceControl` 创建 `Surface`,并将句柄传递给应用进程的 `ViewRootImpl`。\n\n3. **绘制绑定** \n `ViewRootImpl` 通过 `relayoutWindow` 更新 `Surface`,在 `performTraversals` 中完成视图的测量、布局、绘制,最终通过 `Surface` 提交帧数据。\n\n4. **显示合成** \n SurfaceFlinger 根据 `Surface` 的缓冲区内容,合成到屏幕。\n\n---","source":"_posts/Android渲染之Surface与ANativeWindow.md","raw":"---\ntitle: Android独立窗口自绘渲染实现\ndate: 2025-05-03 19:19:19\ncategories: \n\t- Android\ntags: \n\t- 渲染\n---\n# 引言\n在Android中,独立窗口自绘渲染,典型应用场景比如SurfaceView与手机双边侧滑返回动画面板(WMS.addView)。最近入职一家新公司,涉及对接更底层渲染实现,具体表现在NDK层,获取一个独立Window窗口,上层用Skia进行绘制,并在Android系统中渲染出来。本文旨在分析WMS.addView链路,明淅渲染关键路径,为后面自定义渲染作支持。\n<!-- more -->\n# 核心类说明\n\n在 Android 系统中,独立窗口自绘渲染,绕不开`Surface` 和 `ANativeWindow` ,二者是与图形渲染和窗口管理相关的核心类,它们的关系和功能可以总结如下:\n\n## **类结构解释**\n```cpp\nclass Surface : public ANativeObjectBase<ANativeWindow, Surface, RefBase>{\n\n}\n```\n- **继承关系**: \n `Surface` 继承自模板类 `ANativeObjectBase<ANativeWindow, Surface, RefBase>`。\n- **模板参数含义**:\n - **`ANativeWindow`**:表示底层的原生窗口接口。\n - **`Surface`**:子类自身(使用 CRTP 模式,允许基类调用子类方法)。\n - **`RefBase`**:Android 的引用计数基类,用于对象生命周期管理。\n\n---\n\n## **Surface 的作用**\n- **图形渲染的抽象层**: \n `Surface` 是 Android 应用与显示系统之间的桥梁,代表一个可绘制的表面。应用程序通过 `Surface` 进行 UI 渲染(例如通过 Canvas 或 OpenGL ES)。\n- **跨进程通信**: \n `Surface` 实现了 `Parcelable` 接口,可跨进程传递(如从应用进程传递到 SurfaceFlinger 服务进程)。\n- **缓冲区管理**: \n 管理图形缓冲区队列(`BufferQueue`),协调生产者和消费者(如应用和 SurfaceFlinger)之间的缓冲区交换。\n\n---\n\n## **ANativeWindow 的作用**\n- **原生窗口的 C 接口**: \n `ANativeWindow` 是 Android NDK 中定义的抽象,提供对底层窗口系统的访问(如通过 `ANativeWindow_fromSurface()` 获取)。\n- **跨平台兼容性**: \n 封装了不同硬件/平台的窗口操作(例如设置缓冲区大小、格式,提交渲染结果)。\n- **与 Surface 的关系**: \n `ANativeWindow` 是 `Surface` 的底层接口,`Surface` 类通过继承 `ANativeObjectBase` 实现了 `ANativeWindow` 的功能。\n\n---\n\n## **两者关系**\n1. **继承与封装**: \n `Surface` 是 `ANativeWindow` 的高层封装,提供更易用的 C++ API,而 `ANativeWindow` 是底层的 C 风格接口。\n2. **功能实现**: \n `Surface` 通过 `ANativeObjectBase` 模板类继承 `ANativeWindow` 的接口,并实现其方法(如 `dequeueBuffer`/`queueBuffer`),最终通过 `RefBase` 管理生命周期。\n3. **使用场景**:\n - **Java/Kotlin 层**:通过 `Surface` 类进行 UI 渲染。\n - **Native 层(NDK)**:通过 `ANativeWindow` 直接操作窗口(例如 Vulkan/OpenGL ES 渲染)。\n\n\n---\n\n## **总结**\n- **`ANativeWindow`**:底层原生窗口接口,提供跨平台的窗口操作(NDK 使用)。\n- **`Surface`**:高层封装,整合 `ANativeWindow` 功能并提供 Android 框架级的渲染管理。\n- **关系**:`Surface` 是 `ANativeWindow` 的面向对象实现,二者共同服务于图形渲染,前者用于 Java/C++ 框架层,后者用于更接近硬件的 NDK 层。\n\n# WMS.addView自绘链路\n\n在Android 10及以后,WMS.addView可以以侧滑返回动画面板实现代码作说明,代码主要在[systemui/navigationbar/gestural](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/),核心类EdgeBackGestureHandler、BackPanelController、BackPanel,这三个组件共同构成了 Android 的边缘返回手势系统,提供流畅的用户体验,是一个WMS.addView应用场景。\n其中ViewCaptureAwareWindowManager以一个独立的Window添加BackPanel(View),BackPanel响应相应滑动事件,用Canvas做自绘实现,具体链路包括:\n\n## **[BackPanelController#setLayoutParams](https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java;drc=61197364367c9e404c7da6900658f1b16c42d0da;bpv=0;bpt=1;l=813)**\n\n```java\n//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler\noverride fun setLayoutParams(layoutParams: WindowManager.LayoutParams) {\n this.layoutParams = layoutParams\n windowManager.addView(mView, layoutParams)\n }\n```\n\nlayoutParams来自EdgeBackGestureHandler#createLayoutParams\n```java\n//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler\n private WindowManager.LayoutParams createLayoutParams() {\n Resources resources = mContext.getResources();\n //TYPE_NAVIGATION_BAR_PANEL: 表示这是一个导航栏面板窗口,优先级低于系统UI但高于普通应用\n //FLAG_NOT_FOCUSABLE: 窗口不能获得焦点\n //FLAG_NOT_TOUCHABLE: 窗口不接收触摸事件\n //FLAG_LAYOUT_IN_SCREEN: 窗口布局在屏幕坐标系中\n WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(\n resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_width),\n resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_height),\n WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,\n WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE\n | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE\n | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,\n PixelFormat.TRANSLUCENT); //半透明\n layoutParams.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel);\n layoutParams.windowAnimations = 0;\n layoutParams.privateFlags |=\n (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS\n | PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION);\n layoutParams.setTitle(TAG + mContext.getDisplayId());\n layoutParams.setFitInsetsTypes(0 /* types */);\n layoutParams.setTrustedOverlay();\n return layoutParams;\n }\n```\n## **WindowManager.addView 绑定Window**\nWindowManager.addView与WMS处理Window绑定。\n1. **WindowManager.addView 调用链**:\n```java\n// frameworks/base/core/java/android/view/WindowManagerImpl.java\npublic void addView(View view, ViewGroup.LayoutParams params) {\n mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);\n}\n```\n\n2. **WindowManagerGlobal 中的处理**:\n```java\n// frameworks/base/core/java/android/view/WindowManagerGlobal.java\npublic void addView(View view, ViewGroup.LayoutParams params,\n Display display, Window parentWindow) {\n \n // 1. 创建 ViewRootImpl\n if (windowlessSession == null) {\n root = new ViewRootImpl(view.getContext(), display);\n } else {\n root = new ViewRootImpl(view.getContext(), display,\n windowlessSession, new WindowlessWindowLayout());\n }\n \n // 2. 保存引用\n mViews.add(view);\n mRoots.add(root);\n mParams.add(wparams);\n\n // 3. 设置参数\n try {\n root.setView(view, wparams, panelParentView, userId);\n } catch (RuntimeException e) {\n throw e;\n }\n}\n```\n\n3. **`ViewRootImpl.setView()` 触发窗口创建**\n在 `setView()` 中,通过 IPC 调用 `WindowManagerService` 创建窗口:\n```java\n// ViewRootImpl.java\npublic void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) {\n // ... 其他逻辑\n try {\n // 通过 mWindowSession (WindowManagerGlobal.getWindowSession())与 WMS 通信\n mWindowSession.addToDisplayAsUser(\n mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId,\n mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets, mControls);\n } catch (RemoteException e) {\n // 处理异常\n }\n}\n//frameworks/base/services/core/java/com/android/server/wm/Session.java\n @Override\n public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,\n int viewVisibility, int displayId, int userId, @InsetsType int requestedVisibleTypes,\n InputChannel outInputChannel, InsetsState outInsetsState,\n InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,\n float[] outSizeCompatScale) {\n return mService.addWindow(this, window, attrs, viewVisibility, displayId, userId,\n requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,\n outAttachedFrame, outSizeCompatScale);\n }\n```\n- **`mWindowSession`** 是 `IWindowSession` 的实例,由 `WindowManagerGlobal` 创建,是 App 进程与 WMS 通信的代理。\n- **`addToDisplayAsUser`** 是 IPC 调用,通知 WMS 创建窗口并分配资源。\n\n在 WMS 服务端,`Session.addToDisplayAsUser()` 最终会创建 `WindowState`:\n```java\n// WindowManagerService.java\npublic int addWindow(Session session, IWindow client, ...) {\n // ... 权限校验、参数处理\n final WindowState win = new WindowState(this, session, client, token, parentWindow,\n appOp[0], seq, attrs, viewVisibility, session.mUid, userId);\n win.mSession.onWindowAdded(win);\n mWindowMap.put(client.asBinder(), win);\n}\n```\n- **`WindowState`** 是 WMS 中窗口的抽象,管理窗口的层级、可见性等。\n\n## **`ViewRootImpl.relayoutWindow` 绑定Surface**\n`SurfaceControl` 的创建实际发生在 **窗口的首次布局(`performTraversals`)阶段**,由 `WindowManagerService` 触发。关键流程如下:\n在客户端(应用进程)的 `ViewRootImpl` 中,通过 IPC 调用 `WindowManagerService` 的 `relayoutWindow` 方法,请求更新窗口布局并创建 `Surface`:\n```java\n// frameworks/base/core/java/android/view/ViewRootImpl.java\nprivate final WindowRelayoutResult mRelayoutResult = new WindowRelayoutResult(\n mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls);\nprivate int relayoutWindow(WindowManager.LayoutParams params, ...) throws RemoteException {\n // 调用 WMS 的 relayoutWindow 方法\n relayoutResult = mWindowSession.relayout(mWindow, params,\n requestedWidth, requestedHeight, viewVisibility,\n insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0,\n mRelayoutSeq, mLastSyncSeqId, mRelayoutResult);\n if (mSurfaceControl.isValid()) {\n updateBlastSurfaceIfNeeded();\n }\n // ...\n}\n\n// Android 图形系统的底层优化,引入 BLAST (BufferQueue Layer State Traversal) 机制\nvoid updateBlastSurfaceIfNeeded() {\n mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl,\n mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);\n mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);\n mBlastBufferQueue.setApplyToken(mBbqApplyToken);\n Surface blastSurface;\n if (addSchandleToVriSurface()) {\n blastSurface = mBlastBufferQueue.createSurfaceWithHandle();\n } else {\n blastSurface = mBlastBufferQueue.createSurface();\n }\n // 更新 Surface\n mSurface.transferFrom(blastSurface);\n}\n//createSurfaceWithHandle或createSurface调用nativeGetSurface\n// frameworks/base/libs/hwui/BLASTBufferQueue.cpp\n\n// JNI 转换\n//通过 nativePtr 获取 Native 层的 BLASTBufferQueue 实例。\n//调用 getSurface() 获取 sp<Surface>。\n//使用 android_view_Surface_createFromSurface 将 Native Surface 转换为 Java Surface 对象。\nstatic jobject BLASTBufferQueue_getSurface(JNIEnv* env, jclass clazz, jlong nativePtr) {\n BLASTBufferQueue* bbq = reinterpret_cast<BLASTBufferQueue*>(nativePtr);\n sp<Surface> surface = bbq->getSurface();\n return android_view_Surface_createFromSurface(env, surface);\n}\n\n// frameworks/native/libs/gui/BLASTBufferQueue.cpp\n//Surface 是 ANativeWindow 的子类,其底层通过 IGraphicBufferProducer(生产者接口)与 BufferQueue 绑定。在 BLASTBufferQueue 中,生产者接口 mProducer 被传递给 Surface,使其能够通过 dequeueBuffer 和 queueBuffer 管理图形缓冲区\n//Surface 内部持有 IGraphicBufferProducer\n//BBQSurface继承Surface\nsp<Surface> BLASTBufferQueue::getSurface(bool includeSurfaceControlHandle) {\n std::lock_guard _lock{mMutex};\n sp<IBinder> scHandle = nullptr;\n if (includeSurfaceControlHandle && mSurfaceControl) {\n scHandle = mSurfaceControl->getHandle();\n }\n return new BBQSurface(mProducer, true, scHandle, this);\n}\n```\n在服务端(`WindowManagerService`),`mWindowSession.relayout` 最终会调用 `WindowStateAnimator.createSurfaceLocked()` 创建 `SurfaceControl`:\n```java\n//frameworks/base/services/core/java/com/android/server/wm/Session.java\n @Override\npublic int relayout(IWindow window, WindowManager.LayoutParams attrs,\n int requestedWidth, int requestedHeight, int viewFlags, int flags, int seq,\n int lastSyncSeqId, WindowRelayoutResult outRelayoutResult) {\n int res = mService.relayoutWindow(this, window, attrs, requestedWidth,\n requestedHeight, viewFlags, flags, seq, lastSyncSeqId, outRelayoutResult);\n return res;\n}\n// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java\npublic int relayoutWindow(Session session, IWindow client, LayoutParams attrs,\n int requestedWidth, int requestedHeight, int viewVisibility, int flags, int seq,\n int lastSyncSeqId, WindowRelayoutResult outRelayoutResult) {\n result = createSurfaceControl(outSurfaceControl, result, win, winAnimator); \n}\n\n// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java\nprivate int createSurfaceControl(SurfaceControl outSurfaceControl, int result,\n WindowState win, WindowStateAnimator winAnimator) {\n if (!win.mHasSurface) {\n result |= RELAYOUT_RES_SURFACE_CHANGED;\n }\n\n SurfaceControl surfaceControl;\n try {\n Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, \"createSurfaceControl\");\n surfaceControl = winAnimator.createSurfaceLocked();\n } finally {\n Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);\n }\n return result;\n}\n// frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java\nvoid createSurfaceLocked() {\n mSurfaceControl = mWin.makeSurface()\n .setParent(mWin.mSurfaceControl)\n .setName(mTitle)\n .setFormat(format)\n .setFlags(flags)\n .setMetadata(METADATA_WINDOW_TYPE, attrs.type)\n .setMetadata(METADATA_OWNER_UID, mSession.mUid)\n .setMetadata(METADATA_OWNER_PID, mSession.mPid)\n .setCallsite(\"WindowSurfaceController\")\n .setBLASTLayer().build();\n}\n```\n\n## **视图通过 `Surface` 绘制内容**\n在 `ViewRootImpl` 的绘制流程中,通过 `Surface` 提交帧数据:\n```java\n// ViewRootImpl.java -> performTraversals()\nprivate void performTraversals() {\n // 1. 测量、布局\n measureHierarchy(...);\n layout(...);\n // 2. 绘制到 Surface\n performDraw(...)\n}\n\nprivate boolean performDraw(@Nullable SurfaceSyncGroup surfaceSyncGroup) {\n usingAsyncReport = draw(fullRedrawNeeded, surfaceSyncGroup, mSyncBuffer);\n}\n\nprivate boolean draw(boolean fullRedrawNeeded, @Nullable SurfaceSyncGroup activeSyncGroup,\n boolean syncBuffer) {\n if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,\n scalingRequired, dirty, surfaceInsets)) {\n return false;\n }\n}\n\nprivate boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,\n boolean scalingRequired, Rect dirty, Rect surfaceInsets) {\n Surface surface = mSurface;\n Canvas canvas = surface.lockCanvas(dirty);\n // 通过 View 系统绘制内容\n mView.draw(canvas);\n surface.unlockCanvasAndPost(canvas);\n}\n```\n- **`surface.lockCanvas()`** 获取 `Canvas` 对象,用于绘制。\n- **`unlockCanvasAndPost()`** 提交绘制内容到 `Surface`,最终由 SurfaceFlinger 合成显示。\n\n## **总结流程**\n1. **`addView` 触发 `ViewRootImpl.setView()`** \n 客户端通过 IPC 调用 `WMS.addWindow()`,创建 `WindowState`。\n\n2. **首次布局请求** \n `ViewRootImpl.relayoutWindow()` 调用 `WMS.relayoutWindow()`,触发 `SurfaceControl` 的创建。\n\n3. **`SurfaceControl` 的创建** \n 服务端通过 `SurfaceSession` 创建 `SurfaceControl`,并通过 Binder 将句柄返回客户端。\n\n4. **客户端 `Surface` 绑定** \n 客户端 `ViewRootImpl` 通过 `updateBlastSurfaceIfNeeded` 将 `Surface` 与 `SurfaceControl` 绑定。\n\n5. **绘制提交** \n 客户端通过 `Surface.lockCanvas()` 和 `unlockCanvasAndPost()` 将内容绘制到 `Surface`,由 SurfaceFlinger 合成显示。\n\n---\n\n## **关键绑定关系总结**\n| **组件** | **作用** |\n|--------------------|-------------------------------------------------------------------------|\n| `ViewRootImpl` | 连接视图系统与 WMS,管理 `Surface` 生命周期和绘制流程。 |\n| `WindowState` | WMS 中的窗口抽象,持有 `SurfaceControl` 控制 Surface 属性。 |\n| `SurfaceControl` | 服务端(WMS)对 `Surface` 的控制句柄,管理缓冲区分配和属性,对应 SurfaceFlinger 的 Layer。 |\n| `Surface` | 客户端(应用进程)的绘制接口,通过 Binder 持有服务端 `SurfaceControl` 的引用。 |\n\n---\n\n## **流程总结**\n1. **窗口创建** \n `addView` 触发 `ViewRootImpl` 创建,并通过 IPC 调用 WMS 的 `addToDisplayAsUser`,在服务端生成 `WindowState` 和 `SurfaceControl`。\n\n2. **Surface 分配** \n WMS 通过 `SurfaceControl` 创建 `Surface`,并将句柄传递给应用进程的 `ViewRootImpl`。\n\n3. **绘制绑定** \n `ViewRootImpl` 通过 `relayoutWindow` 更新 `Surface`,在 `performTraversals` 中完成视图的测量、布局、绘制,最终通过 `Surface` 提交帧数据。\n\n4. **显示合成** \n SurfaceFlinger 根据 `Surface` 的缓冲区内容,合成到屏幕。\n\n---","slug":"Android渲染之Surface与ANativeWindow","published":1,"updated":"2025-06-02T13:15:33.844Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n830001cate3g5ofogl","content":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>在Android中,独立窗口自绘渲染,典型应用场景比如SurfaceView与手机双边侧滑返回动画面板(WMS.addView)。最近入职一家新公司,涉及对接更底层渲染实现,具体表现在NDK层,获取一个独立Window窗口,上层用Skia进行绘制,并在Android系统中渲染出来。本文旨在分析WMS.addView链路,明淅渲染关键路径,为后面自定义渲染作支持。</p>\n<span id=\"more\"></span>\n<h1 id=\"核心类说明\"><a href=\"#核心类说明\" class=\"headerlink\" title=\"核心类说明\"></a>核心类说明</h1><p>在 Android 系统中,独立窗口自绘渲染,绕不开<code>Surface</code> 和 <code>ANativeWindow</code> ,二者是与图形渲染和窗口管理相关的核心类,它们的关系和功能可以总结如下:</p>\n<h2 id=\"类结构解释\"><a href=\"#类结构解释\" class=\"headerlink\" title=\"类结构解释\"></a><strong>类结构解释</strong></h2><figure class=\"highlight cpp\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">class</span> <span class=\"title class_\">Surface</span> : <span class=\"keyword\">public</span> ANativeObjectBase<ANativeWindow, Surface, RefBase>{</span><br><span class=\"line\"></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong>继承关系</strong>:<br><code>Surface</code> 继承自模板类 <code>ANativeObjectBase<ANativeWindow, Surface, RefBase></code>。</li>\n<li><strong>模板参数含义</strong>:<ul>\n<li><strong><code>ANativeWindow</code></strong>:表示底层的原生窗口接口。</li>\n<li><strong><code>Surface</code></strong>:子类自身(使用 CRTP 模式,允许基类调用子类方法)。</li>\n<li><strong><code>RefBase</code></strong>:Android 的引用计数基类,用于对象生命周期管理。</li>\n</ul>\n</li>\n</ul>\n<hr>\n<h2 id=\"Surface-的作用\"><a href=\"#Surface-的作用\" class=\"headerlink\" title=\"Surface 的作用\"></a><strong>Surface 的作用</strong></h2><ul>\n<li><strong>图形渲染的抽象层</strong>:<br><code>Surface</code> 是 Android 应用与显示系统之间的桥梁,代表一个可绘制的表面。应用程序通过 <code>Surface</code> 进行 UI 渲染(例如通过 Canvas 或 OpenGL ES)。</li>\n<li><strong>跨进程通信</strong>:<br><code>Surface</code> 实现了 <code>Parcelable</code> 接口,可跨进程传递(如从应用进程传递到 SurfaceFlinger 服务进程)。</li>\n<li><strong>缓冲区管理</strong>:<br>管理图形缓冲区队列(<code>BufferQueue</code>),协调生产者和消费者(如应用和 SurfaceFlinger)之间的缓冲区交换。</li>\n</ul>\n<hr>\n<h2 id=\"ANativeWindow-的作用\"><a href=\"#ANativeWindow-的作用\" class=\"headerlink\" title=\"ANativeWindow 的作用\"></a><strong>ANativeWindow 的作用</strong></h2><ul>\n<li><strong>原生窗口的 C 接口</strong>:<br><code>ANativeWindow</code> 是 Android NDK 中定义的抽象,提供对底层窗口系统的访问(如通过 <code>ANativeWindow_fromSurface()</code> 获取)。</li>\n<li><strong>跨平台兼容性</strong>:<br>封装了不同硬件/平台的窗口操作(例如设置缓冲区大小、格式,提交渲染结果)。</li>\n<li><strong>与 Surface 的关系</strong>:<br><code>ANativeWindow</code> 是 <code>Surface</code> 的底层接口,<code>Surface</code> 类通过继承 <code>ANativeObjectBase</code> 实现了 <code>ANativeWindow</code> 的功能。</li>\n</ul>\n<hr>\n<h2 id=\"两者关系\"><a href=\"#两者关系\" class=\"headerlink\" title=\"两者关系\"></a><strong>两者关系</strong></h2><ol>\n<li><strong>继承与封装</strong>:<br><code>Surface</code> 是 <code>ANativeWindow</code> 的高层封装,提供更易用的 C++ API,而 <code>ANativeWindow</code> 是底层的 C 风格接口。</li>\n<li><strong>功能实现</strong>:<br><code>Surface</code> 通过 <code>ANativeObjectBase</code> 模板类继承 <code>ANativeWindow</code> 的接口,并实现其方法(如 <code>dequeueBuffer</code>/<code>queueBuffer</code>),最终通过 <code>RefBase</code> 管理生命周期。</li>\n<li><strong>使用场景</strong>:<ul>\n<li><strong>Java/Kotlin 层</strong>:通过 <code>Surface</code> 类进行 UI 渲染。</li>\n<li><strong>Native 层(NDK)</strong>:通过 <code>ANativeWindow</code> 直接操作窗口(例如 Vulkan/OpenGL ES 渲染)。</li>\n</ul>\n</li>\n</ol>\n<hr>\n<h2 id=\"总结\"><a href=\"#总结\" class=\"headerlink\" title=\"总结\"></a><strong>总结</strong></h2><ul>\n<li><strong><code>ANativeWindow</code></strong>:底层原生窗口接口,提供跨平台的窗口操作(NDK 使用)。</li>\n<li><strong><code>Surface</code></strong>:高层封装,整合 <code>ANativeWindow</code> 功能并提供 Android 框架级的渲染管理。</li>\n<li><strong>关系</strong>:<code>Surface</code> 是 <code>ANativeWindow</code> 的面向对象实现,二者共同服务于图形渲染,前者用于 Java/C++ 框架层,后者用于更接近硬件的 NDK 层。</li>\n</ul>\n<h1 id=\"WMS-addView自绘链路\"><a href=\"#WMS-addView自绘链路\" class=\"headerlink\" title=\"WMS.addView自绘链路\"></a>WMS.addView自绘链路</h1><p>在Android 10及以后,WMS.addView可以以侧滑返回动画面板实现代码作说明,代码主要在<a href=\"https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/\">systemui/navigationbar/gestural</a>,核心类EdgeBackGestureHandler、BackPanelController、BackPanel,这三个组件共同构成了 Android 的边缘返回手势系统,提供流畅的用户体验,是一个WMS.addView应用场景。<br>其中ViewCaptureAwareWindowManager以一个独立的Window添加BackPanel(View),BackPanel响应相应滑动事件,用Canvas做自绘实现,具体链路包括:</p>\n<h2 id=\"BackPanelController-setLayoutParams\"><a href=\"#BackPanelController-setLayoutParams\" class=\"headerlink\" title=\"BackPanelController#setLayoutParams\"></a><strong><a href=\"https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java;drc=61197364367c9e404c7da6900658f1b16c42d0da;bpv=0;bpt=1;l=813\">BackPanelController#setLayoutParams</a></strong></h2><figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler</span></span><br><span class=\"line\">override fun <span class=\"title function_\">setLayoutParams</span><span class=\"params\">(layoutParams: WindowManager.LayoutParams)</span> {</span><br><span class=\"line\"> <span class=\"built_in\">this</span>.layoutParams = layoutParams</span><br><span class=\"line\"> windowManager.addView(mView, layoutParams)</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure>\n\n<p>layoutParams来自EdgeBackGestureHandler#createLayoutParams</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler</span></span><br><span class=\"line\"> <span class=\"keyword\">private</span> WindowManager.LayoutParams <span class=\"title function_\">createLayoutParams</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> <span class=\"type\">Resources</span> <span class=\"variable\">resources</span> <span class=\"operator\">=</span> mContext.getResources();</span><br><span class=\"line\"> <span class=\"comment\">//TYPE_NAVIGATION_BAR_PANEL: 表示这是一个导航栏面板窗口,优先级低于系统UI但高于普通应用</span></span><br><span class=\"line\"> <span class=\"comment\">//FLAG_NOT_FOCUSABLE: 窗口不能获得焦点</span></span><br><span class=\"line\"> <span class=\"comment\">//FLAG_NOT_TOUCHABLE: 窗口不接收触摸事件</span></span><br><span class=\"line\"> <span class=\"comment\">//FLAG_LAYOUT_IN_SCREEN: 窗口布局在屏幕坐标系中</span></span><br><span class=\"line\"> WindowManager.<span class=\"type\">LayoutParams</span> <span class=\"variable\">layoutParams</span> <span class=\"operator\">=</span> <span class=\"keyword\">new</span> <span class=\"title class_\">WindowManager</span>.LayoutParams(</span><br><span class=\"line\"> resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_width),</span><br><span class=\"line\"> resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_height),</span><br><span class=\"line\"> WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,</span><br><span class=\"line\"> WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE</span><br><span class=\"line\"> | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE</span><br><span class=\"line\"> | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,</span><br><span class=\"line\"> PixelFormat.TRANSLUCENT); <span class=\"comment\">//半透明</span></span><br><span class=\"line\"> layoutParams.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel);</span><br><span class=\"line\"> layoutParams.windowAnimations = <span class=\"number\">0</span>;</span><br><span class=\"line\"> layoutParams.privateFlags |=</span><br><span class=\"line\"> (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS</span><br><span class=\"line\"> | PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION);</span><br><span class=\"line\"> layoutParams.setTitle(TAG + mContext.getDisplayId());</span><br><span class=\"line\"> layoutParams.setFitInsetsTypes(<span class=\"number\">0</span> <span class=\"comment\">/* types */</span>);</span><br><span class=\"line\"> layoutParams.setTrustedOverlay();</span><br><span class=\"line\"> <span class=\"keyword\">return</span> layoutParams;</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure>\n<h2 id=\"WindowManager-addView-绑定Window\"><a href=\"#WindowManager-addView-绑定Window\" class=\"headerlink\" title=\"WindowManager.addView 绑定Window\"></a><strong>WindowManager.addView 绑定Window</strong></h2><p>WindowManager.addView与WMS处理Window绑定。</p>\n<ol>\n<li><strong>WindowManager.addView 调用链</strong>:</li>\n</ol>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// frameworks/base/core/java/android/view/WindowManagerImpl.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"keyword\">void</span> <span class=\"title function_\">addView</span><span class=\"params\">(View view, ViewGroup.LayoutParams params)</span> {</span><br><span class=\"line\"> mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<ol start=\"2\">\n<li><strong>WindowManagerGlobal 中的处理</strong>:</li>\n</ol>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// frameworks/base/core/java/android/view/WindowManagerGlobal.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"keyword\">void</span> <span class=\"title function_\">addView</span><span class=\"params\">(View view, ViewGroup.LayoutParams params,</span></span><br><span class=\"line\"><span class=\"params\"> Display display, Window parentWindow)</span> {</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\">// 1. 创建 ViewRootImpl</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (windowlessSession == <span class=\"literal\">null</span>) {</span><br><span class=\"line\"> root = <span class=\"keyword\">new</span> <span class=\"title class_\">ViewRootImpl</span>(view.getContext(), display);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> root = <span class=\"keyword\">new</span> <span class=\"title class_\">ViewRootImpl</span>(view.getContext(), display,</span><br><span class=\"line\"> windowlessSession, <span class=\"keyword\">new</span> <span class=\"title class_\">WindowlessWindowLayout</span>());</span><br><span class=\"line\"> }</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\">// 2. 保存引用</span></span><br><span class=\"line\"> mViews.add(view);</span><br><span class=\"line\"> mRoots.add(root);</span><br><span class=\"line\"> mParams.add(wparams);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 3. 设置参数</span></span><br><span class=\"line\"> <span class=\"keyword\">try</span> {</span><br><span class=\"line\"> root.setView(view, wparams, panelParentView, userId);</span><br><span class=\"line\"> } <span class=\"keyword\">catch</span> (RuntimeException e) {</span><br><span class=\"line\"> <span class=\"keyword\">throw</span> e;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<ol start=\"3\">\n<li><strong><code>ViewRootImpl.setView()</code> 触发窗口创建</strong><br>在 <code>setView()</code> 中,通过 IPC 调用 <code>WindowManagerService</code> 创建窗口:</li>\n</ol>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// ViewRootImpl.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"keyword\">void</span> <span class=\"title function_\">setView</span><span class=\"params\">(View view, WindowManager.LayoutParams attrs, View panelParentView, <span class=\"type\">int</span> userId)</span> {</span><br><span class=\"line\"> <span class=\"comment\">// ... 其他逻辑</span></span><br><span class=\"line\"> <span class=\"keyword\">try</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 通过 mWindowSession (WindowManagerGlobal.getWindowSession())与 WMS 通信</span></span><br><span class=\"line\"> mWindowSession.addToDisplayAsUser(</span><br><span class=\"line\"> mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId,</span><br><span class=\"line\"> mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets, mControls);</span><br><span class=\"line\"> } <span class=\"keyword\">catch</span> (RemoteException e) {</span><br><span class=\"line\"> <span class=\"comment\">// 处理异常</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">//frameworks/base/services/core/java/com/android/server/wm/Session.java</span></span><br><span class=\"line\"> <span class=\"meta\">@Override</span></span><br><span class=\"line\"> <span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">addToDisplayAsUser</span><span class=\"params\">(IWindow window, WindowManager.LayoutParams attrs,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> viewVisibility, <span class=\"type\">int</span> displayId, <span class=\"type\">int</span> userId, <span class=\"meta\">@InsetsType</span> <span class=\"type\">int</span> requestedVisibleTypes,</span></span><br><span class=\"line\"><span class=\"params\"> InputChannel outInputChannel, InsetsState outInsetsState,</span></span><br><span class=\"line\"><span class=\"params\"> InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">float</span>[] outSizeCompatScale)</span> {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> mService.addWindow(<span class=\"built_in\">this</span>, window, attrs, viewVisibility, displayId, userId,</span><br><span class=\"line\"> requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,</span><br><span class=\"line\"> outAttachedFrame, outSizeCompatScale);</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong><code>mWindowSession</code></strong> 是 <code>IWindowSession</code> 的实例,由 <code>WindowManagerGlobal</code> 创建,是 App 进程与 WMS 通信的代理。</li>\n<li><strong><code>addToDisplayAsUser</code></strong> 是 IPC 调用,通知 WMS 创建窗口并分配资源。</li>\n</ul>\n<p>在 WMS 服务端,<code>Session.addToDisplayAsUser()</code> 最终会创建 <code>WindowState</code>:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// WindowManagerService.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">addWindow</span><span class=\"params\">(Session session, IWindow client, ...)</span> {</span><br><span class=\"line\"> <span class=\"comment\">// ... 权限校验、参数处理</span></span><br><span class=\"line\"> <span class=\"keyword\">final</span> <span class=\"type\">WindowState</span> <span class=\"variable\">win</span> <span class=\"operator\">=</span> <span class=\"keyword\">new</span> <span class=\"title class_\">WindowState</span>(<span class=\"built_in\">this</span>, session, client, token, parentWindow,</span><br><span class=\"line\"> appOp[<span class=\"number\">0</span>], seq, attrs, viewVisibility, session.mUid, userId);</span><br><span class=\"line\"> win.mSession.onWindowAdded(win);</span><br><span class=\"line\"> mWindowMap.put(client.asBinder(), win);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong><code>WindowState</code></strong> 是 WMS 中窗口的抽象,管理窗口的层级、可见性等。</li>\n</ul>\n<h2 id=\"ViewRootImpl-relayoutWindow-绑定Surface\"><a href=\"#ViewRootImpl-relayoutWindow-绑定Surface\" class=\"headerlink\" title=\"ViewRootImpl.relayoutWindow 绑定Surface\"></a><strong><code>ViewRootImpl.relayoutWindow</code> 绑定Surface</strong></h2><p><code>SurfaceControl</code> 的创建实际发生在 <strong>窗口的首次布局(<code>performTraversals</code>)阶段</strong>,由 <code>WindowManagerService</code> 触发。关键流程如下:<br>在客户端(应用进程)的 <code>ViewRootImpl</code> 中,通过 IPC 调用 <code>WindowManagerService</code> 的 <code>relayoutWindow</code> 方法,请求更新窗口布局并创建 <code>Surface</code>:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// frameworks/base/core/java/android/view/ViewRootImpl.java</span></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"keyword\">final</span> <span class=\"type\">WindowRelayoutResult</span> <span class=\"variable\">mRelayoutResult</span> <span class=\"operator\">=</span> <span class=\"keyword\">new</span> <span class=\"title class_\">WindowRelayoutResult</span>(</span><br><span class=\"line\"> mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls);</span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">int</span> <span class=\"title function_\">relayoutWindow</span><span class=\"params\">(WindowManager.LayoutParams params, ...)</span> <span class=\"keyword\">throws</span> RemoteException {</span><br><span class=\"line\"> <span class=\"comment\">// 调用 WMS 的 relayoutWindow 方法</span></span><br><span class=\"line\"> relayoutResult = mWindowSession.relayout(mWindow, params,</span><br><span class=\"line\"> requestedWidth, requestedHeight, viewVisibility,</span><br><span class=\"line\"> insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : <span class=\"number\">0</span>,</span><br><span class=\"line\"> mRelayoutSeq, mLastSyncSeqId, mRelayoutResult);</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (mSurfaceControl.isValid()) {</span><br><span class=\"line\"> updateBlastSurfaceIfNeeded();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"comment\">// ...</span></span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// Android 图形系统的底层优化,引入 BLAST (BufferQueue Layer State Traversal) 机制</span></span><br><span class=\"line\"><span class=\"keyword\">void</span> <span class=\"title function_\">updateBlastSurfaceIfNeeded</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> mBlastBufferQueue = <span class=\"keyword\">new</span> <span class=\"title class_\">BLASTBufferQueue</span>(mTag, mSurfaceControl,</span><br><span class=\"line\"> mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);</span><br><span class=\"line\"> mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);</span><br><span class=\"line\"> mBlastBufferQueue.setApplyToken(mBbqApplyToken);</span><br><span class=\"line\"> Surface blastSurface;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (addSchandleToVriSurface()) {</span><br><span class=\"line\"> blastSurface = mBlastBufferQueue.createSurfaceWithHandle();</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> blastSurface = mBlastBufferQueue.createSurface();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"comment\">// 更新 Surface</span></span><br><span class=\"line\"> mSurface.transferFrom(blastSurface);</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">//createSurfaceWithHandle或createSurface调用nativeGetSurface</span></span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/libs/hwui/BLASTBufferQueue.cpp</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// JNI 转换</span></span><br><span class=\"line\"><span class=\"comment\">//通过 nativePtr 获取 Native 层的 BLASTBufferQueue 实例。</span></span><br><span class=\"line\"><span class=\"comment\">//调用 getSurface() 获取 sp<Surface>。</span></span><br><span class=\"line\"><span class=\"comment\">//使用 android_view_Surface_createFromSurface 将 Native Surface 转换为 Java Surface 对象。</span></span><br><span class=\"line\"><span class=\"keyword\">static</span> jobject <span class=\"title function_\">BLASTBufferQueue_getSurface</span><span class=\"params\">(JNIEnv* env, jclass clazz, jlong nativePtr)</span> {</span><br><span class=\"line\"> BLASTBufferQueue* bbq = reinterpret_cast<BLASTBufferQueue*>(nativePtr);</span><br><span class=\"line\"> sp<Surface> surface = bbq->getSurface();</span><br><span class=\"line\"> <span class=\"keyword\">return</span> android_view_Surface_createFromSurface(env, surface);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// frameworks/native/libs/gui/BLASTBufferQueue.cpp</span></span><br><span class=\"line\"><span class=\"comment\">//Surface 是 ANativeWindow 的子类,其底层通过 IGraphicBufferProducer(生产者接口)与 BufferQueue 绑定。在 BLASTBufferQueue 中,生产者接口 mProducer 被传递给 Surface,使其能够通过 dequeueBuffer 和 queueBuffer 管理图形缓冲区</span></span><br><span class=\"line\"><span class=\"comment\">//Surface 内部持有 IGraphicBufferProducer</span></span><br><span class=\"line\"><span class=\"comment\">//BBQSurface继承Surface</span></span><br><span class=\"line\">sp<Surface> BLASTBufferQueue::getSurface(bool includeSurfaceControlHandle) {</span><br><span class=\"line\"> std::lock_guard _lock{mMutex};</span><br><span class=\"line\"> sp<IBinder> scHandle = nullptr;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (includeSurfaceControlHandle && mSurfaceControl) {</span><br><span class=\"line\"> scHandle = mSurfaceControl->getHandle();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"keyword\">new</span> <span class=\"title class_\">BBQSurface</span>(mProducer, <span class=\"literal\">true</span>, scHandle, <span class=\"built_in\">this</span>);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>在服务端(<code>WindowManagerService</code>),<code>mWindowSession.relayout</code> 最终会调用 <code>WindowStateAnimator.createSurfaceLocked()</code> 创建 <code>SurfaceControl</code>:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//frameworks/base/services/core/java/com/android/server/wm/Session.java</span></span><br><span class=\"line\"> <span class=\"meta\">@Override</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">relayout</span><span class=\"params\">(IWindow window, WindowManager.LayoutParams attrs,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> requestedWidth, <span class=\"type\">int</span> requestedHeight, <span class=\"type\">int</span> viewFlags, <span class=\"type\">int</span> flags, <span class=\"type\">int</span> seq,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> lastSyncSeqId, WindowRelayoutResult outRelayoutResult)</span> {</span><br><span class=\"line\"> <span class=\"type\">int</span> <span class=\"variable\">res</span> <span class=\"operator\">=</span> mService.relayoutWindow(<span class=\"built_in\">this</span>, window, attrs, requestedWidth,</span><br><span class=\"line\"> requestedHeight, viewFlags, flags, seq, lastSyncSeqId, outRelayoutResult);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> res;</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">relayoutWindow</span><span class=\"params\">(Session session, IWindow client, LayoutParams attrs,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> requestedWidth, <span class=\"type\">int</span> requestedHeight, <span class=\"type\">int</span> viewVisibility, <span class=\"type\">int</span> flags, <span class=\"type\">int</span> seq,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> lastSyncSeqId, WindowRelayoutResult outRelayoutResult)</span> {</span><br><span class=\"line\"> result = createSurfaceControl(outSurfaceControl, result, win, winAnimator); </span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java</span></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">int</span> <span class=\"title function_\">createSurfaceControl</span><span class=\"params\">(SurfaceControl outSurfaceControl, <span class=\"type\">int</span> result,</span></span><br><span class=\"line\"><span class=\"params\"> WindowState win, WindowStateAnimator winAnimator)</span> {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (!win.mHasSurface) {</span><br><span class=\"line\"> result |= RELAYOUT_RES_SURFACE_CHANGED;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> SurfaceControl surfaceControl;</span><br><span class=\"line\"> <span class=\"keyword\">try</span> {</span><br><span class=\"line\"> Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, <span class=\"string\">"createSurfaceControl"</span>);</span><br><span class=\"line\"> surfaceControl = winAnimator.createSurfaceLocked();</span><br><span class=\"line\"> } <span class=\"keyword\">finally</span> {</span><br><span class=\"line\"> Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> result;</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java</span></span><br><span class=\"line\"><span class=\"keyword\">void</span> <span class=\"title function_\">createSurfaceLocked</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> mSurfaceControl = mWin.makeSurface()</span><br><span class=\"line\"> .setParent(mWin.mSurfaceControl)</span><br><span class=\"line\"> .setName(mTitle)</span><br><span class=\"line\"> .setFormat(format)</span><br><span class=\"line\"> .setFlags(flags)</span><br><span class=\"line\"> .setMetadata(METADATA_WINDOW_TYPE, attrs.type)</span><br><span class=\"line\"> .setMetadata(METADATA_OWNER_UID, mSession.mUid)</span><br><span class=\"line\"> .setMetadata(METADATA_OWNER_PID, mSession.mPid)</span><br><span class=\"line\"> .setCallsite(<span class=\"string\">"WindowSurfaceController"</span>)</span><br><span class=\"line\"> .setBLASTLayer().build();</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"视图通过-Surface-绘制内容\"><a href=\"#视图通过-Surface-绘制内容\" class=\"headerlink\" title=\"视图通过 Surface 绘制内容\"></a><strong>视图通过 <code>Surface</code> 绘制内容</strong></h2><p>在 <code>ViewRootImpl</code> 的绘制流程中,通过 <code>Surface</code> 提交帧数据:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// ViewRootImpl.java -> performTraversals()</span></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"keyword\">void</span> <span class=\"title function_\">performTraversals</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 1. 测量、布局</span></span><br><span class=\"line\"> measureHierarchy(...);</span><br><span class=\"line\"> layout(...);</span><br><span class=\"line\"> <span class=\"comment\">// 2. 绘制到 Surface</span></span><br><span class=\"line\"> performDraw(...)</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">boolean</span> <span class=\"title function_\">performDraw</span><span class=\"params\">(<span class=\"meta\">@Nullable</span> SurfaceSyncGroup surfaceSyncGroup)</span> {</span><br><span class=\"line\"> usingAsyncReport = draw(fullRedrawNeeded, surfaceSyncGroup, mSyncBuffer);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">boolean</span> <span class=\"title function_\">draw</span><span class=\"params\">(<span class=\"type\">boolean</span> fullRedrawNeeded, <span class=\"meta\">@Nullable</span> SurfaceSyncGroup activeSyncGroup,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">boolean</span> syncBuffer)</span> {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,</span><br><span class=\"line\"> scalingRequired, dirty, surfaceInsets)) {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">false</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">boolean</span> <span class=\"title function_\">drawSoftware</span><span class=\"params\">(Surface surface, AttachInfo attachInfo, <span class=\"type\">int</span> xoff, <span class=\"type\">int</span> yoff,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">boolean</span> scalingRequired, Rect dirty, Rect surfaceInsets)</span> {</span><br><span class=\"line\"> <span class=\"type\">Surface</span> <span class=\"variable\">surface</span> <span class=\"operator\">=</span> mSurface;</span><br><span class=\"line\"> <span class=\"type\">Canvas</span> <span class=\"variable\">canvas</span> <span class=\"operator\">=</span> surface.lockCanvas(dirty);</span><br><span class=\"line\"> <span class=\"comment\">// 通过 View 系统绘制内容</span></span><br><span class=\"line\"> mView.draw(canvas);</span><br><span class=\"line\"> surface.unlockCanvasAndPost(canvas);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong><code>surface.lockCanvas()</code></strong> 获取 <code>Canvas</code> 对象,用于绘制。</li>\n<li><strong><code>unlockCanvasAndPost()</code></strong> 提交绘制内容到 <code>Surface</code>,最终由 SurfaceFlinger 合成显示。</li>\n</ul>\n<h2 id=\"总结流程\"><a href=\"#总结流程\" class=\"headerlink\" title=\"总结流程\"></a><strong>总结流程</strong></h2><ol>\n<li><p><strong><code>addView</code> 触发 <code>ViewRootImpl.setView()</code></strong><br>客户端通过 IPC 调用 <code>WMS.addWindow()</code>,创建 <code>WindowState</code>。</p>\n</li>\n<li><p><strong>首次布局请求</strong><br><code>ViewRootImpl.relayoutWindow()</code> 调用 <code>WMS.relayoutWindow()</code>,触发 <code>SurfaceControl</code> 的创建。</p>\n</li>\n<li><p><strong><code>SurfaceControl</code> 的创建</strong><br>服务端通过 <code>SurfaceSession</code> 创建 <code>SurfaceControl</code>,并通过 Binder 将句柄返回客户端。</p>\n</li>\n<li><p><strong>客户端 <code>Surface</code> 绑定</strong><br>客户端 <code>ViewRootImpl</code> 通过 <code>updateBlastSurfaceIfNeeded</code> 将 <code>Surface</code> 与 <code>SurfaceControl</code> 绑定。</p>\n</li>\n<li><p><strong>绘制提交</strong><br>客户端通过 <code>Surface.lockCanvas()</code> 和 <code>unlockCanvasAndPost()</code> 将内容绘制到 <code>Surface</code>,由 SurfaceFlinger 合成显示。</p>\n</li>\n</ol>\n<hr>\n<h2 id=\"关键绑定关系总结\"><a href=\"#关键绑定关系总结\" class=\"headerlink\" title=\"关键绑定关系总结\"></a><strong>关键绑定关系总结</strong></h2><table>\n<thead>\n<tr>\n<th><strong>组件</strong></th>\n<th><strong>作用</strong></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>ViewRootImpl</code></td>\n<td>连接视图系统与 WMS,管理 <code>Surface</code> 生命周期和绘制流程。</td>\n</tr>\n<tr>\n<td><code>WindowState</code></td>\n<td>WMS 中的窗口抽象,持有 <code>SurfaceControl</code> 控制 Surface 属性。</td>\n</tr>\n<tr>\n<td><code>SurfaceControl</code></td>\n<td>服务端(WMS)对 <code>Surface</code> 的控制句柄,管理缓冲区分配和属性,对应 SurfaceFlinger 的 Layer。</td>\n</tr>\n<tr>\n<td><code>Surface</code></td>\n<td>客户端(应用进程)的绘制接口,通过 Binder 持有服务端 <code>SurfaceControl</code> 的引用。</td>\n</tr>\n</tbody></table>\n<hr>\n<h2 id=\"流程总结\"><a href=\"#流程总结\" class=\"headerlink\" title=\"流程总结\"></a><strong>流程总结</strong></h2><ol>\n<li><p><strong>窗口创建</strong><br><code>addView</code> 触发 <code>ViewRootImpl</code> 创建,并通过 IPC 调用 WMS 的 <code>addToDisplayAsUser</code>,在服务端生成 <code>WindowState</code> 和 <code>SurfaceControl</code>。</p>\n</li>\n<li><p><strong>Surface 分配</strong><br>WMS 通过 <code>SurfaceControl</code> 创建 <code>Surface</code>,并将句柄传递给应用进程的 <code>ViewRootImpl</code>。</p>\n</li>\n<li><p><strong>绘制绑定</strong><br><code>ViewRootImpl</code> 通过 <code>relayoutWindow</code> 更新 <code>Surface</code>,在 <code>performTraversals</code> 中完成视图的测量、布局、绘制,最终通过 <code>Surface</code> 提交帧数据。</p>\n</li>\n<li><p><strong>显示合成</strong><br>SurfaceFlinger 根据 <code>Surface</code> 的缓冲区内容,合成到屏幕。</p>\n</li>\n</ol>\n<hr>\n","excerpt":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>在Android中,独立窗口自绘渲染,典型应用场景比如SurfaceView与手机双边侧滑返回动画面板(WMS.addView)。最近入职一家新公司,涉及对接更底层渲染实现,具体表现在NDK层,获取一个独立Window窗口,上层用Skia进行绘制,并在Android系统中渲染出来。本文旨在分析WMS.addView链路,明淅渲染关键路径,为后面自定义渲染作支持。</p>","more":"<h1 id=\"核心类说明\"><a href=\"#核心类说明\" class=\"headerlink\" title=\"核心类说明\"></a>核心类说明</h1><p>在 Android 系统中,独立窗口自绘渲染,绕不开<code>Surface</code> 和 <code>ANativeWindow</code> ,二者是与图形渲染和窗口管理相关的核心类,它们的关系和功能可以总结如下:</p>\n<h2 id=\"类结构解释\"><a href=\"#类结构解释\" class=\"headerlink\" title=\"类结构解释\"></a><strong>类结构解释</strong></h2><figure class=\"highlight cpp\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">class</span> <span class=\"title class_\">Surface</span> : <span class=\"keyword\">public</span> ANativeObjectBase<ANativeWindow, Surface, RefBase>{</span><br><span class=\"line\"></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong>继承关系</strong>:<br><code>Surface</code> 继承自模板类 <code>ANativeObjectBase<ANativeWindow, Surface, RefBase></code>。</li>\n<li><strong>模板参数含义</strong>:<ul>\n<li><strong><code>ANativeWindow</code></strong>:表示底层的原生窗口接口。</li>\n<li><strong><code>Surface</code></strong>:子类自身(使用 CRTP 模式,允许基类调用子类方法)。</li>\n<li><strong><code>RefBase</code></strong>:Android 的引用计数基类,用于对象生命周期管理。</li>\n</ul>\n</li>\n</ul>\n<hr>\n<h2 id=\"Surface-的作用\"><a href=\"#Surface-的作用\" class=\"headerlink\" title=\"Surface 的作用\"></a><strong>Surface 的作用</strong></h2><ul>\n<li><strong>图形渲染的抽象层</strong>:<br><code>Surface</code> 是 Android 应用与显示系统之间的桥梁,代表一个可绘制的表面。应用程序通过 <code>Surface</code> 进行 UI 渲染(例如通过 Canvas 或 OpenGL ES)。</li>\n<li><strong>跨进程通信</strong>:<br><code>Surface</code> 实现了 <code>Parcelable</code> 接口,可跨进程传递(如从应用进程传递到 SurfaceFlinger 服务进程)。</li>\n<li><strong>缓冲区管理</strong>:<br>管理图形缓冲区队列(<code>BufferQueue</code>),协调生产者和消费者(如应用和 SurfaceFlinger)之间的缓冲区交换。</li>\n</ul>\n<hr>\n<h2 id=\"ANativeWindow-的作用\"><a href=\"#ANativeWindow-的作用\" class=\"headerlink\" title=\"ANativeWindow 的作用\"></a><strong>ANativeWindow 的作用</strong></h2><ul>\n<li><strong>原生窗口的 C 接口</strong>:<br><code>ANativeWindow</code> 是 Android NDK 中定义的抽象,提供对底层窗口系统的访问(如通过 <code>ANativeWindow_fromSurface()</code> 获取)。</li>\n<li><strong>跨平台兼容性</strong>:<br>封装了不同硬件/平台的窗口操作(例如设置缓冲区大小、格式,提交渲染结果)。</li>\n<li><strong>与 Surface 的关系</strong>:<br><code>ANativeWindow</code> 是 <code>Surface</code> 的底层接口,<code>Surface</code> 类通过继承 <code>ANativeObjectBase</code> 实现了 <code>ANativeWindow</code> 的功能。</li>\n</ul>\n<hr>\n<h2 id=\"两者关系\"><a href=\"#两者关系\" class=\"headerlink\" title=\"两者关系\"></a><strong>两者关系</strong></h2><ol>\n<li><strong>继承与封装</strong>:<br><code>Surface</code> 是 <code>ANativeWindow</code> 的高层封装,提供更易用的 C++ API,而 <code>ANativeWindow</code> 是底层的 C 风格接口。</li>\n<li><strong>功能实现</strong>:<br><code>Surface</code> 通过 <code>ANativeObjectBase</code> 模板类继承 <code>ANativeWindow</code> 的接口,并实现其方法(如 <code>dequeueBuffer</code>/<code>queueBuffer</code>),最终通过 <code>RefBase</code> 管理生命周期。</li>\n<li><strong>使用场景</strong>:<ul>\n<li><strong>Java/Kotlin 层</strong>:通过 <code>Surface</code> 类进行 UI 渲染。</li>\n<li><strong>Native 层(NDK)</strong>:通过 <code>ANativeWindow</code> 直接操作窗口(例如 Vulkan/OpenGL ES 渲染)。</li>\n</ul>\n</li>\n</ol>\n<hr>\n<h2 id=\"总结\"><a href=\"#总结\" class=\"headerlink\" title=\"总结\"></a><strong>总结</strong></h2><ul>\n<li><strong><code>ANativeWindow</code></strong>:底层原生窗口接口,提供跨平台的窗口操作(NDK 使用)。</li>\n<li><strong><code>Surface</code></strong>:高层封装,整合 <code>ANativeWindow</code> 功能并提供 Android 框架级的渲染管理。</li>\n<li><strong>关系</strong>:<code>Surface</code> 是 <code>ANativeWindow</code> 的面向对象实现,二者共同服务于图形渲染,前者用于 Java/C++ 框架层,后者用于更接近硬件的 NDK 层。</li>\n</ul>\n<h1 id=\"WMS-addView自绘链路\"><a href=\"#WMS-addView自绘链路\" class=\"headerlink\" title=\"WMS.addView自绘链路\"></a>WMS.addView自绘链路</h1><p>在Android 10及以后,WMS.addView可以以侧滑返回动画面板实现代码作说明,代码主要在<a href=\"https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/\">systemui/navigationbar/gestural</a>,核心类EdgeBackGestureHandler、BackPanelController、BackPanel,这三个组件共同构成了 Android 的边缘返回手势系统,提供流畅的用户体验,是一个WMS.addView应用场景。<br>其中ViewCaptureAwareWindowManager以一个独立的Window添加BackPanel(View),BackPanel响应相应滑动事件,用Canvas做自绘实现,具体链路包括:</p>\n<h2 id=\"BackPanelController-setLayoutParams\"><a href=\"#BackPanelController-setLayoutParams\" class=\"headerlink\" title=\"BackPanelController#setLayoutParams\"></a><strong><a href=\"https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java;drc=61197364367c9e404c7da6900658f1b16c42d0da;bpv=0;bpt=1;l=813\">BackPanelController#setLayoutParams</a></strong></h2><figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler</span></span><br><span class=\"line\">override fun <span class=\"title function_\">setLayoutParams</span><span class=\"params\">(layoutParams: WindowManager.LayoutParams)</span> {</span><br><span class=\"line\"> <span class=\"built_in\">this</span>.layoutParams = layoutParams</span><br><span class=\"line\"> windowManager.addView(mView, layoutParams)</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure>\n\n<p>layoutParams来自EdgeBackGestureHandler#createLayoutParams</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler</span></span><br><span class=\"line\"> <span class=\"keyword\">private</span> WindowManager.LayoutParams <span class=\"title function_\">createLayoutParams</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> <span class=\"type\">Resources</span> <span class=\"variable\">resources</span> <span class=\"operator\">=</span> mContext.getResources();</span><br><span class=\"line\"> <span class=\"comment\">//TYPE_NAVIGATION_BAR_PANEL: 表示这是一个导航栏面板窗口,优先级低于系统UI但高于普通应用</span></span><br><span class=\"line\"> <span class=\"comment\">//FLAG_NOT_FOCUSABLE: 窗口不能获得焦点</span></span><br><span class=\"line\"> <span class=\"comment\">//FLAG_NOT_TOUCHABLE: 窗口不接收触摸事件</span></span><br><span class=\"line\"> <span class=\"comment\">//FLAG_LAYOUT_IN_SCREEN: 窗口布局在屏幕坐标系中</span></span><br><span class=\"line\"> WindowManager.<span class=\"type\">LayoutParams</span> <span class=\"variable\">layoutParams</span> <span class=\"operator\">=</span> <span class=\"keyword\">new</span> <span class=\"title class_\">WindowManager</span>.LayoutParams(</span><br><span class=\"line\"> resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_width),</span><br><span class=\"line\"> resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_height),</span><br><span class=\"line\"> WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,</span><br><span class=\"line\"> WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE</span><br><span class=\"line\"> | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE</span><br><span class=\"line\"> | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,</span><br><span class=\"line\"> PixelFormat.TRANSLUCENT); <span class=\"comment\">//半透明</span></span><br><span class=\"line\"> layoutParams.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel);</span><br><span class=\"line\"> layoutParams.windowAnimations = <span class=\"number\">0</span>;</span><br><span class=\"line\"> layoutParams.privateFlags |=</span><br><span class=\"line\"> (WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS</span><br><span class=\"line\"> | PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION);</span><br><span class=\"line\"> layoutParams.setTitle(TAG + mContext.getDisplayId());</span><br><span class=\"line\"> layoutParams.setFitInsetsTypes(<span class=\"number\">0</span> <span class=\"comment\">/* types */</span>);</span><br><span class=\"line\"> layoutParams.setTrustedOverlay();</span><br><span class=\"line\"> <span class=\"keyword\">return</span> layoutParams;</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure>\n<h2 id=\"WindowManager-addView-绑定Window\"><a href=\"#WindowManager-addView-绑定Window\" class=\"headerlink\" title=\"WindowManager.addView 绑定Window\"></a><strong>WindowManager.addView 绑定Window</strong></h2><p>WindowManager.addView与WMS处理Window绑定。</p>\n<ol>\n<li><strong>WindowManager.addView 调用链</strong>:</li>\n</ol>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// frameworks/base/core/java/android/view/WindowManagerImpl.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"keyword\">void</span> <span class=\"title function_\">addView</span><span class=\"params\">(View view, ViewGroup.LayoutParams params)</span> {</span><br><span class=\"line\"> mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<ol start=\"2\">\n<li><strong>WindowManagerGlobal 中的处理</strong>:</li>\n</ol>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// frameworks/base/core/java/android/view/WindowManagerGlobal.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"keyword\">void</span> <span class=\"title function_\">addView</span><span class=\"params\">(View view, ViewGroup.LayoutParams params,</span></span><br><span class=\"line\"><span class=\"params\"> Display display, Window parentWindow)</span> {</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\">// 1. 创建 ViewRootImpl</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (windowlessSession == <span class=\"literal\">null</span>) {</span><br><span class=\"line\"> root = <span class=\"keyword\">new</span> <span class=\"title class_\">ViewRootImpl</span>(view.getContext(), display);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> root = <span class=\"keyword\">new</span> <span class=\"title class_\">ViewRootImpl</span>(view.getContext(), display,</span><br><span class=\"line\"> windowlessSession, <span class=\"keyword\">new</span> <span class=\"title class_\">WindowlessWindowLayout</span>());</span><br><span class=\"line\"> }</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\">// 2. 保存引用</span></span><br><span class=\"line\"> mViews.add(view);</span><br><span class=\"line\"> mRoots.add(root);</span><br><span class=\"line\"> mParams.add(wparams);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 3. 设置参数</span></span><br><span class=\"line\"> <span class=\"keyword\">try</span> {</span><br><span class=\"line\"> root.setView(view, wparams, panelParentView, userId);</span><br><span class=\"line\"> } <span class=\"keyword\">catch</span> (RuntimeException e) {</span><br><span class=\"line\"> <span class=\"keyword\">throw</span> e;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<ol start=\"3\">\n<li><strong><code>ViewRootImpl.setView()</code> 触发窗口创建</strong><br>在 <code>setView()</code> 中,通过 IPC 调用 <code>WindowManagerService</code> 创建窗口:</li>\n</ol>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// ViewRootImpl.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"keyword\">void</span> <span class=\"title function_\">setView</span><span class=\"params\">(View view, WindowManager.LayoutParams attrs, View panelParentView, <span class=\"type\">int</span> userId)</span> {</span><br><span class=\"line\"> <span class=\"comment\">// ... 其他逻辑</span></span><br><span class=\"line\"> <span class=\"keyword\">try</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 通过 mWindowSession (WindowManagerGlobal.getWindowSession())与 WMS 通信</span></span><br><span class=\"line\"> mWindowSession.addToDisplayAsUser(</span><br><span class=\"line\"> mWindow, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), userId,</span><br><span class=\"line\"> mInsetsController.getRequestedVisibility(), inputChannel, mTempInsets, mControls);</span><br><span class=\"line\"> } <span class=\"keyword\">catch</span> (RemoteException e) {</span><br><span class=\"line\"> <span class=\"comment\">// 处理异常</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">//frameworks/base/services/core/java/com/android/server/wm/Session.java</span></span><br><span class=\"line\"> <span class=\"meta\">@Override</span></span><br><span class=\"line\"> <span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">addToDisplayAsUser</span><span class=\"params\">(IWindow window, WindowManager.LayoutParams attrs,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> viewVisibility, <span class=\"type\">int</span> displayId, <span class=\"type\">int</span> userId, <span class=\"meta\">@InsetsType</span> <span class=\"type\">int</span> requestedVisibleTypes,</span></span><br><span class=\"line\"><span class=\"params\"> InputChannel outInputChannel, InsetsState outInsetsState,</span></span><br><span class=\"line\"><span class=\"params\"> InsetsSourceControl.Array outActiveControls, Rect outAttachedFrame,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">float</span>[] outSizeCompatScale)</span> {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> mService.addWindow(<span class=\"built_in\">this</span>, window, attrs, viewVisibility, displayId, userId,</span><br><span class=\"line\"> requestedVisibleTypes, outInputChannel, outInsetsState, outActiveControls,</span><br><span class=\"line\"> outAttachedFrame, outSizeCompatScale);</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong><code>mWindowSession</code></strong> 是 <code>IWindowSession</code> 的实例,由 <code>WindowManagerGlobal</code> 创建,是 App 进程与 WMS 通信的代理。</li>\n<li><strong><code>addToDisplayAsUser</code></strong> 是 IPC 调用,通知 WMS 创建窗口并分配资源。</li>\n</ul>\n<p>在 WMS 服务端,<code>Session.addToDisplayAsUser()</code> 最终会创建 <code>WindowState</code>:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// WindowManagerService.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">addWindow</span><span class=\"params\">(Session session, IWindow client, ...)</span> {</span><br><span class=\"line\"> <span class=\"comment\">// ... 权限校验、参数处理</span></span><br><span class=\"line\"> <span class=\"keyword\">final</span> <span class=\"type\">WindowState</span> <span class=\"variable\">win</span> <span class=\"operator\">=</span> <span class=\"keyword\">new</span> <span class=\"title class_\">WindowState</span>(<span class=\"built_in\">this</span>, session, client, token, parentWindow,</span><br><span class=\"line\"> appOp[<span class=\"number\">0</span>], seq, attrs, viewVisibility, session.mUid, userId);</span><br><span class=\"line\"> win.mSession.onWindowAdded(win);</span><br><span class=\"line\"> mWindowMap.put(client.asBinder(), win);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong><code>WindowState</code></strong> 是 WMS 中窗口的抽象,管理窗口的层级、可见性等。</li>\n</ul>\n<h2 id=\"ViewRootImpl-relayoutWindow-绑定Surface\"><a href=\"#ViewRootImpl-relayoutWindow-绑定Surface\" class=\"headerlink\" title=\"ViewRootImpl.relayoutWindow 绑定Surface\"></a><strong><code>ViewRootImpl.relayoutWindow</code> 绑定Surface</strong></h2><p><code>SurfaceControl</code> 的创建实际发生在 <strong>窗口的首次布局(<code>performTraversals</code>)阶段</strong>,由 <code>WindowManagerService</code> 触发。关键流程如下:<br>在客户端(应用进程)的 <code>ViewRootImpl</code> 中,通过 IPC 调用 <code>WindowManagerService</code> 的 <code>relayoutWindow</code> 方法,请求更新窗口布局并创建 <code>Surface</code>:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// frameworks/base/core/java/android/view/ViewRootImpl.java</span></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"keyword\">final</span> <span class=\"type\">WindowRelayoutResult</span> <span class=\"variable\">mRelayoutResult</span> <span class=\"operator\">=</span> <span class=\"keyword\">new</span> <span class=\"title class_\">WindowRelayoutResult</span>(</span><br><span class=\"line\"> mTmpFrames, mPendingMergedConfiguration, mSurfaceControl, mTempInsets, mTempControls);</span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">int</span> <span class=\"title function_\">relayoutWindow</span><span class=\"params\">(WindowManager.LayoutParams params, ...)</span> <span class=\"keyword\">throws</span> RemoteException {</span><br><span class=\"line\"> <span class=\"comment\">// 调用 WMS 的 relayoutWindow 方法</span></span><br><span class=\"line\"> relayoutResult = mWindowSession.relayout(mWindow, params,</span><br><span class=\"line\"> requestedWidth, requestedHeight, viewVisibility,</span><br><span class=\"line\"> insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : <span class=\"number\">0</span>,</span><br><span class=\"line\"> mRelayoutSeq, mLastSyncSeqId, mRelayoutResult);</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (mSurfaceControl.isValid()) {</span><br><span class=\"line\"> updateBlastSurfaceIfNeeded();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"comment\">// ...</span></span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// Android 图形系统的底层优化,引入 BLAST (BufferQueue Layer State Traversal) 机制</span></span><br><span class=\"line\"><span class=\"keyword\">void</span> <span class=\"title function_\">updateBlastSurfaceIfNeeded</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> mBlastBufferQueue = <span class=\"keyword\">new</span> <span class=\"title class_\">BLASTBufferQueue</span>(mTag, mSurfaceControl,</span><br><span class=\"line\"> mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format);</span><br><span class=\"line\"> mBlastBufferQueue.setTransactionHangCallback(sTransactionHangCallback);</span><br><span class=\"line\"> mBlastBufferQueue.setApplyToken(mBbqApplyToken);</span><br><span class=\"line\"> Surface blastSurface;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (addSchandleToVriSurface()) {</span><br><span class=\"line\"> blastSurface = mBlastBufferQueue.createSurfaceWithHandle();</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> blastSurface = mBlastBufferQueue.createSurface();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"comment\">// 更新 Surface</span></span><br><span class=\"line\"> mSurface.transferFrom(blastSurface);</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">//createSurfaceWithHandle或createSurface调用nativeGetSurface</span></span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/libs/hwui/BLASTBufferQueue.cpp</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// JNI 转换</span></span><br><span class=\"line\"><span class=\"comment\">//通过 nativePtr 获取 Native 层的 BLASTBufferQueue 实例。</span></span><br><span class=\"line\"><span class=\"comment\">//调用 getSurface() 获取 sp<Surface>。</span></span><br><span class=\"line\"><span class=\"comment\">//使用 android_view_Surface_createFromSurface 将 Native Surface 转换为 Java Surface 对象。</span></span><br><span class=\"line\"><span class=\"keyword\">static</span> jobject <span class=\"title function_\">BLASTBufferQueue_getSurface</span><span class=\"params\">(JNIEnv* env, jclass clazz, jlong nativePtr)</span> {</span><br><span class=\"line\"> BLASTBufferQueue* bbq = reinterpret_cast<BLASTBufferQueue*>(nativePtr);</span><br><span class=\"line\"> sp<Surface> surface = bbq->getSurface();</span><br><span class=\"line\"> <span class=\"keyword\">return</span> android_view_Surface_createFromSurface(env, surface);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// frameworks/native/libs/gui/BLASTBufferQueue.cpp</span></span><br><span class=\"line\"><span class=\"comment\">//Surface 是 ANativeWindow 的子类,其底层通过 IGraphicBufferProducer(生产者接口)与 BufferQueue 绑定。在 BLASTBufferQueue 中,生产者接口 mProducer 被传递给 Surface,使其能够通过 dequeueBuffer 和 queueBuffer 管理图形缓冲区</span></span><br><span class=\"line\"><span class=\"comment\">//Surface 内部持有 IGraphicBufferProducer</span></span><br><span class=\"line\"><span class=\"comment\">//BBQSurface继承Surface</span></span><br><span class=\"line\">sp<Surface> BLASTBufferQueue::getSurface(bool includeSurfaceControlHandle) {</span><br><span class=\"line\"> std::lock_guard _lock{mMutex};</span><br><span class=\"line\"> sp<IBinder> scHandle = nullptr;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (includeSurfaceControlHandle && mSurfaceControl) {</span><br><span class=\"line\"> scHandle = mSurfaceControl->getHandle();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"keyword\">new</span> <span class=\"title class_\">BBQSurface</span>(mProducer, <span class=\"literal\">true</span>, scHandle, <span class=\"built_in\">this</span>);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>在服务端(<code>WindowManagerService</code>),<code>mWindowSession.relayout</code> 最终会调用 <code>WindowStateAnimator.createSurfaceLocked()</code> 创建 <code>SurfaceControl</code>:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//frameworks/base/services/core/java/com/android/server/wm/Session.java</span></span><br><span class=\"line\"> <span class=\"meta\">@Override</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">relayout</span><span class=\"params\">(IWindow window, WindowManager.LayoutParams attrs,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> requestedWidth, <span class=\"type\">int</span> requestedHeight, <span class=\"type\">int</span> viewFlags, <span class=\"type\">int</span> flags, <span class=\"type\">int</span> seq,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> lastSyncSeqId, WindowRelayoutResult outRelayoutResult)</span> {</span><br><span class=\"line\"> <span class=\"type\">int</span> <span class=\"variable\">res</span> <span class=\"operator\">=</span> mService.relayoutWindow(<span class=\"built_in\">this</span>, window, attrs, requestedWidth,</span><br><span class=\"line\"> requestedHeight, viewFlags, flags, seq, lastSyncSeqId, outRelayoutResult);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> res;</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"type\">int</span> <span class=\"title function_\">relayoutWindow</span><span class=\"params\">(Session session, IWindow client, LayoutParams attrs,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> requestedWidth, <span class=\"type\">int</span> requestedHeight, <span class=\"type\">int</span> viewVisibility, <span class=\"type\">int</span> flags, <span class=\"type\">int</span> seq,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">int</span> lastSyncSeqId, WindowRelayoutResult outRelayoutResult)</span> {</span><br><span class=\"line\"> result = createSurfaceControl(outSurfaceControl, result, win, winAnimator); </span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java</span></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">int</span> <span class=\"title function_\">createSurfaceControl</span><span class=\"params\">(SurfaceControl outSurfaceControl, <span class=\"type\">int</span> result,</span></span><br><span class=\"line\"><span class=\"params\"> WindowState win, WindowStateAnimator winAnimator)</span> {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (!win.mHasSurface) {</span><br><span class=\"line\"> result |= RELAYOUT_RES_SURFACE_CHANGED;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> SurfaceControl surfaceControl;</span><br><span class=\"line\"> <span class=\"keyword\">try</span> {</span><br><span class=\"line\"> Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, <span class=\"string\">"createSurfaceControl"</span>);</span><br><span class=\"line\"> surfaceControl = winAnimator.createSurfaceLocked();</span><br><span class=\"line\"> } <span class=\"keyword\">finally</span> {</span><br><span class=\"line\"> Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> result;</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">// frameworks/base/services/core/java/com/android/server/wm/WindowStateAnimator.java</span></span><br><span class=\"line\"><span class=\"keyword\">void</span> <span class=\"title function_\">createSurfaceLocked</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> mSurfaceControl = mWin.makeSurface()</span><br><span class=\"line\"> .setParent(mWin.mSurfaceControl)</span><br><span class=\"line\"> .setName(mTitle)</span><br><span class=\"line\"> .setFormat(format)</span><br><span class=\"line\"> .setFlags(flags)</span><br><span class=\"line\"> .setMetadata(METADATA_WINDOW_TYPE, attrs.type)</span><br><span class=\"line\"> .setMetadata(METADATA_OWNER_UID, mSession.mUid)</span><br><span class=\"line\"> .setMetadata(METADATA_OWNER_PID, mSession.mPid)</span><br><span class=\"line\"> .setCallsite(<span class=\"string\">"WindowSurfaceController"</span>)</span><br><span class=\"line\"> .setBLASTLayer().build();</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"视图通过-Surface-绘制内容\"><a href=\"#视图通过-Surface-绘制内容\" class=\"headerlink\" title=\"视图通过 Surface 绘制内容\"></a><strong>视图通过 <code>Surface</code> 绘制内容</strong></h2><p>在 <code>ViewRootImpl</code> 的绘制流程中,通过 <code>Surface</code> 提交帧数据:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// ViewRootImpl.java -> performTraversals()</span></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"keyword\">void</span> <span class=\"title function_\">performTraversals</span><span class=\"params\">()</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 1. 测量、布局</span></span><br><span class=\"line\"> measureHierarchy(...);</span><br><span class=\"line\"> layout(...);</span><br><span class=\"line\"> <span class=\"comment\">// 2. 绘制到 Surface</span></span><br><span class=\"line\"> performDraw(...)</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">boolean</span> <span class=\"title function_\">performDraw</span><span class=\"params\">(<span class=\"meta\">@Nullable</span> SurfaceSyncGroup surfaceSyncGroup)</span> {</span><br><span class=\"line\"> usingAsyncReport = draw(fullRedrawNeeded, surfaceSyncGroup, mSyncBuffer);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">boolean</span> <span class=\"title function_\">draw</span><span class=\"params\">(<span class=\"type\">boolean</span> fullRedrawNeeded, <span class=\"meta\">@Nullable</span> SurfaceSyncGroup activeSyncGroup,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">boolean</span> syncBuffer)</span> {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (!drawSoftware(surface, mAttachInfo, xOffset, yOffset,</span><br><span class=\"line\"> scalingRequired, dirty, surfaceInsets)) {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">false</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">private</span> <span class=\"type\">boolean</span> <span class=\"title function_\">drawSoftware</span><span class=\"params\">(Surface surface, AttachInfo attachInfo, <span class=\"type\">int</span> xoff, <span class=\"type\">int</span> yoff,</span></span><br><span class=\"line\"><span class=\"params\"> <span class=\"type\">boolean</span> scalingRequired, Rect dirty, Rect surfaceInsets)</span> {</span><br><span class=\"line\"> <span class=\"type\">Surface</span> <span class=\"variable\">surface</span> <span class=\"operator\">=</span> mSurface;</span><br><span class=\"line\"> <span class=\"type\">Canvas</span> <span class=\"variable\">canvas</span> <span class=\"operator\">=</span> surface.lockCanvas(dirty);</span><br><span class=\"line\"> <span class=\"comment\">// 通过 View 系统绘制内容</span></span><br><span class=\"line\"> mView.draw(canvas);</span><br><span class=\"line\"> surface.unlockCanvasAndPost(canvas);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<ul>\n<li><strong><code>surface.lockCanvas()</code></strong> 获取 <code>Canvas</code> 对象,用于绘制。</li>\n<li><strong><code>unlockCanvasAndPost()</code></strong> 提交绘制内容到 <code>Surface</code>,最终由 SurfaceFlinger 合成显示。</li>\n</ul>\n<h2 id=\"总结流程\"><a href=\"#总结流程\" class=\"headerlink\" title=\"总结流程\"></a><strong>总结流程</strong></h2><ol>\n<li><p><strong><code>addView</code> 触发 <code>ViewRootImpl.setView()</code></strong><br>客户端通过 IPC 调用 <code>WMS.addWindow()</code>,创建 <code>WindowState</code>。</p>\n</li>\n<li><p><strong>首次布局请求</strong><br><code>ViewRootImpl.relayoutWindow()</code> 调用 <code>WMS.relayoutWindow()</code>,触发 <code>SurfaceControl</code> 的创建。</p>\n</li>\n<li><p><strong><code>SurfaceControl</code> 的创建</strong><br>服务端通过 <code>SurfaceSession</code> 创建 <code>SurfaceControl</code>,并通过 Binder 将句柄返回客户端。</p>\n</li>\n<li><p><strong>客户端 <code>Surface</code> 绑定</strong><br>客户端 <code>ViewRootImpl</code> 通过 <code>updateBlastSurfaceIfNeeded</code> 将 <code>Surface</code> 与 <code>SurfaceControl</code> 绑定。</p>\n</li>\n<li><p><strong>绘制提交</strong><br>客户端通过 <code>Surface.lockCanvas()</code> 和 <code>unlockCanvasAndPost()</code> 将内容绘制到 <code>Surface</code>,由 SurfaceFlinger 合成显示。</p>\n</li>\n</ol>\n<hr>\n<h2 id=\"关键绑定关系总结\"><a href=\"#关键绑定关系总结\" class=\"headerlink\" title=\"关键绑定关系总结\"></a><strong>关键绑定关系总结</strong></h2><table>\n<thead>\n<tr>\n<th><strong>组件</strong></th>\n<th><strong>作用</strong></th>\n</tr>\n</thead>\n<tbody><tr>\n<td><code>ViewRootImpl</code></td>\n<td>连接视图系统与 WMS,管理 <code>Surface</code> 生命周期和绘制流程。</td>\n</tr>\n<tr>\n<td><code>WindowState</code></td>\n<td>WMS 中的窗口抽象,持有 <code>SurfaceControl</code> 控制 Surface 属性。</td>\n</tr>\n<tr>\n<td><code>SurfaceControl</code></td>\n<td>服务端(WMS)对 <code>Surface</code> 的控制句柄,管理缓冲区分配和属性,对应 SurfaceFlinger 的 Layer。</td>\n</tr>\n<tr>\n<td><code>Surface</code></td>\n<td>客户端(应用进程)的绘制接口,通过 Binder 持有服务端 <code>SurfaceControl</code> 的引用。</td>\n</tr>\n</tbody></table>\n<hr>\n<h2 id=\"流程总结\"><a href=\"#流程总结\" class=\"headerlink\" title=\"流程总结\"></a><strong>流程总结</strong></h2><ol>\n<li><p><strong>窗口创建</strong><br><code>addView</code> 触发 <code>ViewRootImpl</code> 创建,并通过 IPC 调用 WMS 的 <code>addToDisplayAsUser</code>,在服务端生成 <code>WindowState</code> 和 <code>SurfaceControl</code>。</p>\n</li>\n<li><p><strong>Surface 分配</strong><br>WMS 通过 <code>SurfaceControl</code> 创建 <code>Surface</code>,并将句柄传递给应用进程的 <code>ViewRootImpl</code>。</p>\n</li>\n<li><p><strong>绘制绑定</strong><br><code>ViewRootImpl</code> 通过 <code>relayoutWindow</code> 更新 <code>Surface</code>,在 <code>performTraversals</code> 中完成视图的测量、布局、绘制,最终通过 <code>Surface</code> 提交帧数据。</p>\n</li>\n<li><p><strong>显示合成</strong><br>SurfaceFlinger 根据 <code>Surface</code> 的缓冲区内容,合成到屏幕。</p>\n</li>\n</ol>\n<hr>"},{"title":"MacOS环境下Flutter Engine编译纪要","date":"2020-03-27T01:29:48.000Z","_content":"## 引言\n由于网络和电脑存储问题,一直未在本地编译过engine。近期时间稍有富余,便着手在macOS环境下编译[futter engine](https://github.com/flutter/engine)工程,以方便阅读engine源码和定制化engine。编译flutter不复杂,只是在国内,我们需要翻墙开放给gclient等工具下载源码。本文仅记录在参考flutter wiki [Setting-up-the-Engine-development-environment](https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment)下,碰到的问题,以及给出解决方案,不对依赖工具作安装和介绍。\n<!-- more -->\n## 过程纪要\n### gclient sync失败问题\n安装依赖工具,新建engine目录并配置.gclient文件后,我们执行gclient sync操作,这时我们可能会遇到http代理和engine/src/flutter/DEPS配置文件问题,具体如下。\n\nhttp代理问题:\n```\nRPC failed transiently. Will retry in 1m4s {\"error\":\"failed to send request: Post https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/GetInstanceURL: EOF\", \"host\":\"chrome-infra-packages.appspot.com\", \"method\":\"GetInstanceURL\", \"service\":\"cipd.Repository\", \"sleepTime\":\"1m4s\"}\n```\n\n上述错误是我们无法访问chrome-infra-packages.appspot.com站点问题,需要配置http代理到我们本地http代理端口,可在命令窗口执行以下操作(其中端口号视本机具体配置改变):\n```\nexport http_proxy=http://127.0.0.1:1089\nexport https_proxy=http://127.0.0.1:1089\n```\n\nengine/src/flutter/DEPS配置文件问题出现在重复配置了'src/third_party/dart/tools/sdks'和'src/third_party/dart/pkg/analysis_server/language_model'两项(基于engine 8d6f008fb commit),解决此错误,我们只需删除以下重复配置即可:\n```\n 'src/third_party/dart/tools/sdks': {\n 'packages': [\n {\n 'package': 'dart/dart-sdk/${{platform}}',\n 'version': 'version:2.4.0'\n }\n ],\n 'dep_type': 'cipd',\n },\n\n 'src/third_party/dart/pkg/analysis_server/language_model': {\n 'packages': [\n {\n 'package': 'dart/language_model',\n 'version': 'EFtZ0Z5T822s4EUOOaWeiXUppRGKp5d9Z6jomJIeQYcC',\n }\n ],\n 'dep_type': 'cipd',\n },\n```\n\ngclient sync过程中,我们不能强制结束gclient sync进程,可能很多小伙伴和我一样,看到以下输出认为gclient sync已结束,但其实不然,gclient还在继续工作。\n```\nSyncing projects: 100% (102/102), done.\n```\n\n### 导入engine源码\n我们可以采用CLion IDE来阅读engine源码,参考flutter wiki [Compiling-the-engine](https://github.com/flutter/flutter/wiki/Compiling-the-engine),执行gn命令生成compile_commands.json文件就可以导入engine源码了。这里以软链方式进行导入:\n>建立软链\n\n```\n在src/futter目录执行\nln -s /Users/xxx/engine/src/out/compile_commands.json /compile_commands.json\n```\n\n>导入CLion\n\nclion打开上一步软链compile_commands.json文件,导入时间可能较长,耐心等待即可。\n \n\n### 引用locally-built engine\n\n引用engine建议采用WIKI [The-flutter-tool](https://github.com/flutter/flutter/wiki/The-flutter-tool) 推荐的参数形式,例如:\n```\nA typical invocation would be: --local-engine-src-path /path/to/engine/src --local-engine=android_debug_unopt.\n```\n当然也可以通过直接替换方式,把flutter SDK bin/cache/artifacts/engine目录下的相应文件进行替换,这里不做推荐。\n\n## 参考\n\n1.Setting-up-the-Engine-development-environment:https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment\n2.Compiling-the-engine: https://github.com/flutter/flutter/wiki/Compiling-the-engine\n","source":"_posts/MacOS环境下Flutter Engine编译纪要.md","raw":"---\ntitle: MacOS环境下Flutter Engine编译纪要\ndate: 2020-03-27 09:29:48\ncategories: \n - Flutter\n---\n## 引言\n由于网络和电脑存储问题,一直未在本地编译过engine。近期时间稍有富余,便着手在macOS环境下编译[futter engine](https://github.com/flutter/engine)工程,以方便阅读engine源码和定制化engine。编译flutter不复杂,只是在国内,我们需要翻墙开放给gclient等工具下载源码。本文仅记录在参考flutter wiki [Setting-up-the-Engine-development-environment](https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment)下,碰到的问题,以及给出解决方案,不对依赖工具作安装和介绍。\n<!-- more -->\n## 过程纪要\n### gclient sync失败问题\n安装依赖工具,新建engine目录并配置.gclient文件后,我们执行gclient sync操作,这时我们可能会遇到http代理和engine/src/flutter/DEPS配置文件问题,具体如下。\n\nhttp代理问题:\n```\nRPC failed transiently. Will retry in 1m4s {\"error\":\"failed to send request: Post https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/GetInstanceURL: EOF\", \"host\":\"chrome-infra-packages.appspot.com\", \"method\":\"GetInstanceURL\", \"service\":\"cipd.Repository\", \"sleepTime\":\"1m4s\"}\n```\n\n上述错误是我们无法访问chrome-infra-packages.appspot.com站点问题,需要配置http代理到我们本地http代理端口,可在命令窗口执行以下操作(其中端口号视本机具体配置改变):\n```\nexport http_proxy=http://127.0.0.1:1089\nexport https_proxy=http://127.0.0.1:1089\n```\n\nengine/src/flutter/DEPS配置文件问题出现在重复配置了'src/third_party/dart/tools/sdks'和'src/third_party/dart/pkg/analysis_server/language_model'两项(基于engine 8d6f008fb commit),解决此错误,我们只需删除以下重复配置即可:\n```\n 'src/third_party/dart/tools/sdks': {\n 'packages': [\n {\n 'package': 'dart/dart-sdk/${{platform}}',\n 'version': 'version:2.4.0'\n }\n ],\n 'dep_type': 'cipd',\n },\n\n 'src/third_party/dart/pkg/analysis_server/language_model': {\n 'packages': [\n {\n 'package': 'dart/language_model',\n 'version': 'EFtZ0Z5T822s4EUOOaWeiXUppRGKp5d9Z6jomJIeQYcC',\n }\n ],\n 'dep_type': 'cipd',\n },\n```\n\ngclient sync过程中,我们不能强制结束gclient sync进程,可能很多小伙伴和我一样,看到以下输出认为gclient sync已结束,但其实不然,gclient还在继续工作。\n```\nSyncing projects: 100% (102/102), done.\n```\n\n### 导入engine源码\n我们可以采用CLion IDE来阅读engine源码,参考flutter wiki [Compiling-the-engine](https://github.com/flutter/flutter/wiki/Compiling-the-engine),执行gn命令生成compile_commands.json文件就可以导入engine源码了。这里以软链方式进行导入:\n>建立软链\n\n```\n在src/futter目录执行\nln -s /Users/xxx/engine/src/out/compile_commands.json /compile_commands.json\n```\n\n>导入CLion\n\nclion打开上一步软链compile_commands.json文件,导入时间可能较长,耐心等待即可。\n \n\n### 引用locally-built engine\n\n引用engine建议采用WIKI [The-flutter-tool](https://github.com/flutter/flutter/wiki/The-flutter-tool) 推荐的参数形式,例如:\n```\nA typical invocation would be: --local-engine-src-path /path/to/engine/src --local-engine=android_debug_unopt.\n```\n当然也可以通过直接替换方式,把flutter SDK bin/cache/artifacts/engine目录下的相应文件进行替换,这里不做推荐。\n\n## 参考\n\n1.Setting-up-the-Engine-development-environment:https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment\n2.Compiling-the-engine: https://github.com/flutter/flutter/wiki/Compiling-the-engine\n","slug":"MacOS环境下Flutter Engine编译纪要","published":1,"updated":"2025-06-02T13:15:33.844Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n860003cate9mqo4x0d","content":"<h2 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h2><p>由于网络和电脑存储问题,一直未在本地编译过engine。近期时间稍有富余,便着手在macOS环境下编译<a href=\"https://github.com/flutter/engine\">futter engine</a>工程,以方便阅读engine源码和定制化engine。编译flutter不复杂,只是在国内,我们需要翻墙开放给gclient等工具下载源码。本文仅记录在参考flutter wiki <a href=\"https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment\">Setting-up-the-Engine-development-environment</a>下,碰到的问题,以及给出解决方案,不对依赖工具作安装和介绍。</p>\n<span id=\"more\"></span>\n<h2 id=\"过程纪要\"><a href=\"#过程纪要\" class=\"headerlink\" title=\"过程纪要\"></a>过程纪要</h2><h3 id=\"gclient-sync失败问题\"><a href=\"#gclient-sync失败问题\" class=\"headerlink\" title=\"gclient sync失败问题\"></a>gclient sync失败问题</h3><p>安装依赖工具,新建engine目录并配置.gclient文件后,我们执行gclient sync操作,这时我们可能会遇到http代理和engine/src/flutter/DEPS配置文件问题,具体如下。</p>\n<p>http代理问题:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">RPC failed transiently. Will retry in 1m4s {"error":"failed to send request: Post https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/GetInstanceURL: EOF", "host":"chrome-infra-packages.appspot.com", "method":"GetInstanceURL", "service":"cipd.Repository", "sleepTime":"1m4s"}</span><br></pre></td></tr></table></figure>\n\n<p>上述错误是我们无法访问chrome-infra-packages.appspot.com站点问题,需要配置http代理到我们本地http代理端口,可在命令窗口执行以下操作(其中端口号视本机具体配置改变):</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">export http_proxy=http://127.0.0.1:1089</span><br><span class=\"line\">export https_proxy=http://127.0.0.1:1089</span><br></pre></td></tr></table></figure>\n\n<p>engine/src/flutter/DEPS配置文件问题出现在重复配置了’src/third_party/dart/tools/sdks’和’src/third_party/dart/pkg/analysis_server/language_model’两项(基于engine 8d6f008fb commit),解决此错误,我们只需删除以下重复配置即可:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">'src/third_party/dart/tools/sdks': {</span><br><span class=\"line\"> 'packages': [</span><br><span class=\"line\"> {</span><br><span class=\"line\"> 'package': 'dart/dart-sdk/${{platform}}',</span><br><span class=\"line\"> 'version': 'version:2.4.0'</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ],</span><br><span class=\"line\"> 'dep_type': 'cipd',</span><br><span class=\"line\">},</span><br><span class=\"line\"></span><br><span class=\"line\">'src/third_party/dart/pkg/analysis_server/language_model': {</span><br><span class=\"line\"> 'packages': [</span><br><span class=\"line\"> {</span><br><span class=\"line\"> 'package': 'dart/language_model',</span><br><span class=\"line\"> 'version': 'EFtZ0Z5T822s4EUOOaWeiXUppRGKp5d9Z6jomJIeQYcC',</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ],</span><br><span class=\"line\"> 'dep_type': 'cipd',</span><br><span class=\"line\">},</span><br></pre></td></tr></table></figure>\n\n<p>gclient sync过程中,我们不能强制结束gclient sync进程,可能很多小伙伴和我一样,看到以下输出认为gclient sync已结束,但其实不然,gclient还在继续工作。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">Syncing projects: 100% (102/102), done.</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"导入engine源码\"><a href=\"#导入engine源码\" class=\"headerlink\" title=\"导入engine源码\"></a>导入engine源码</h3><p>我们可以采用CLion IDE来阅读engine源码,参考flutter wiki <a href=\"https://github.com/flutter/flutter/wiki/Compiling-the-engine\">Compiling-the-engine</a>,执行gn命令生成compile_commands.json文件就可以导入engine源码了。这里以软链方式进行导入:</p>\n<blockquote>\n<p>建立软链</p>\n</blockquote>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">在src/futter目录执行</span><br><span class=\"line\">ln -s /Users/xxx/engine/src/out/compile_commands.json /compile_commands.json</span><br></pre></td></tr></table></figure>\n\n<blockquote>\n<p>导入CLion</p>\n</blockquote>\n<p>clion打开上一步软链compile_commands.json文件,导入时间可能较长,耐心等待即可。<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/clion_engine.png?raw=true\"> </p>\n<h3 id=\"引用locally-built-engine\"><a href=\"#引用locally-built-engine\" class=\"headerlink\" title=\"引用locally-built engine\"></a>引用locally-built engine</h3><p>引用engine建议采用WIKI <a href=\"https://github.com/flutter/flutter/wiki/The-flutter-tool\">The-flutter-tool</a> 推荐的参数形式,例如:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">A typical invocation would be: --local-engine-src-path /path/to/engine/src --local-engine=android_debug_unopt.</span><br></pre></td></tr></table></figure>\n<p>当然也可以通过直接替换方式,把flutter SDK bin/cache/artifacts/engine目录下的相应文件进行替换,这里不做推荐。</p>\n<h2 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h2><p>1.Setting-up-the-Engine-development-environment:<a href=\"https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment\">https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment</a><br>2.Compiling-the-engine: <a href=\"https://github.com/flutter/flutter/wiki/Compiling-the-engine\">https://github.com/flutter/flutter/wiki/Compiling-the-engine</a></p>\n","excerpt":"<h2 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h2><p>由于网络和电脑存储问题,一直未在本地编译过engine。近期时间稍有富余,便着手在macOS环境下编译<a href=\"https://github.com/flutter/engine\">futter engine</a>工程,以方便阅读engine源码和定制化engine。编译flutter不复杂,只是在国内,我们需要翻墙开放给gclient等工具下载源码。本文仅记录在参考flutter wiki <a href=\"https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment\">Setting-up-the-Engine-development-environment</a>下,碰到的问题,以及给出解决方案,不对依赖工具作安装和介绍。</p>","more":"<h2 id=\"过程纪要\"><a href=\"#过程纪要\" class=\"headerlink\" title=\"过程纪要\"></a>过程纪要</h2><h3 id=\"gclient-sync失败问题\"><a href=\"#gclient-sync失败问题\" class=\"headerlink\" title=\"gclient sync失败问题\"></a>gclient sync失败问题</h3><p>安装依赖工具,新建engine目录并配置.gclient文件后,我们执行gclient sync操作,这时我们可能会遇到http代理和engine/src/flutter/DEPS配置文件问题,具体如下。</p>\n<p>http代理问题:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">RPC failed transiently. Will retry in 1m4s {"error":"failed to send request: Post https://chrome-infra-packages.appspot.com/prpc/cipd.Repository/GetInstanceURL: EOF", "host":"chrome-infra-packages.appspot.com", "method":"GetInstanceURL", "service":"cipd.Repository", "sleepTime":"1m4s"}</span><br></pre></td></tr></table></figure>\n\n<p>上述错误是我们无法访问chrome-infra-packages.appspot.com站点问题,需要配置http代理到我们本地http代理端口,可在命令窗口执行以下操作(其中端口号视本机具体配置改变):</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">export http_proxy=http://127.0.0.1:1089</span><br><span class=\"line\">export https_proxy=http://127.0.0.1:1089</span><br></pre></td></tr></table></figure>\n\n<p>engine/src/flutter/DEPS配置文件问题出现在重复配置了’src/third_party/dart/tools/sdks’和’src/third_party/dart/pkg/analysis_server/language_model’两项(基于engine 8d6f008fb commit),解决此错误,我们只需删除以下重复配置即可:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">'src/third_party/dart/tools/sdks': {</span><br><span class=\"line\"> 'packages': [</span><br><span class=\"line\"> {</span><br><span class=\"line\"> 'package': 'dart/dart-sdk/${{platform}}',</span><br><span class=\"line\"> 'version': 'version:2.4.0'</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ],</span><br><span class=\"line\"> 'dep_type': 'cipd',</span><br><span class=\"line\">},</span><br><span class=\"line\"></span><br><span class=\"line\">'src/third_party/dart/pkg/analysis_server/language_model': {</span><br><span class=\"line\"> 'packages': [</span><br><span class=\"line\"> {</span><br><span class=\"line\"> 'package': 'dart/language_model',</span><br><span class=\"line\"> 'version': 'EFtZ0Z5T822s4EUOOaWeiXUppRGKp5d9Z6jomJIeQYcC',</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ],</span><br><span class=\"line\"> 'dep_type': 'cipd',</span><br><span class=\"line\">},</span><br></pre></td></tr></table></figure>\n\n<p>gclient sync过程中,我们不能强制结束gclient sync进程,可能很多小伙伴和我一样,看到以下输出认为gclient sync已结束,但其实不然,gclient还在继续工作。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">Syncing projects: 100% (102/102), done.</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"导入engine源码\"><a href=\"#导入engine源码\" class=\"headerlink\" title=\"导入engine源码\"></a>导入engine源码</h3><p>我们可以采用CLion IDE来阅读engine源码,参考flutter wiki <a href=\"https://github.com/flutter/flutter/wiki/Compiling-the-engine\">Compiling-the-engine</a>,执行gn命令生成compile_commands.json文件就可以导入engine源码了。这里以软链方式进行导入:</p>\n<blockquote>\n<p>建立软链</p>\n</blockquote>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">在src/futter目录执行</span><br><span class=\"line\">ln -s /Users/xxx/engine/src/out/compile_commands.json /compile_commands.json</span><br></pre></td></tr></table></figure>\n\n<blockquote>\n<p>导入CLion</p>\n</blockquote>\n<p>clion打开上一步软链compile_commands.json文件,导入时间可能较长,耐心等待即可。<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/clion_engine.png?raw=true\"> </p>\n<h3 id=\"引用locally-built-engine\"><a href=\"#引用locally-built-engine\" class=\"headerlink\" title=\"引用locally-built engine\"></a>引用locally-built engine</h3><p>引用engine建议采用WIKI <a href=\"https://github.com/flutter/flutter/wiki/The-flutter-tool\">The-flutter-tool</a> 推荐的参数形式,例如:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">A typical invocation would be: --local-engine-src-path /path/to/engine/src --local-engine=android_debug_unopt.</span><br></pre></td></tr></table></figure>\n<p>当然也可以通过直接替换方式,把flutter SDK bin/cache/artifacts/engine目录下的相应文件进行替换,这里不做推荐。</p>\n<h2 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h2><p>1.Setting-up-the-Engine-development-environment:<a href=\"https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment\">https://github.com/flutter/flutter/wiki/Setting-up-the-Engine-development-environment</a><br>2.Compiling-the-engine: <a href=\"https://github.com/flutter/flutter/wiki/Compiling-the-engine\">https://github.com/flutter/flutter/wiki/Compiling-the-engine</a></p>"},{"title":"rokio源码实现简单分析","date":"2025-06-02T08:41:48.000Z","_content":"\n待完成\n\n\n## 前言\n \nTokio 是一个 Rust 的异步运行时,它提供了一个完整的异步 I/O 框架。其实现在AI Code分析工具,比如Cursor、windsurf等基本都能分析出tokio核心实现,本文并不做八股文总结,仅尝试从任务调度实现角度分析,给出我们可以借鉴的设计思想,其中包括:\n- 工作窃取实现\n- 任务调度优化\n- 阻塞任务实现\n\n先来看tokio简单示例:\n- 创建运行时\n```rust\nlet rt = Runtime::builder()\n .worker_threads(4)\n .build()\n .unwrap();\n```\n\n- 提交任务\n```rust\nrt.spawn(async {\n println!(\"Running on worker thread\");\n});\n```\n\n- 提交阻塞任务\n```rust\nrt.spawn_blocking(|| {\n // 执行阻塞操作\n});\n```\n<!-- more -->\n## 整体架构\n作为异步 I/O 框架,其核心架构包含以下几个主要组件:\n### 核心组件\n```rust\npub struct Runtime {\n /// 调度器\n scheduler: Scheduler,\n /// 运行时句柄\n handle: Handle,\n /// 阻塞线程池\n blocking_pool: BlockingPool,\n}\n```\n\n### 调度器类型\n```rust\npub enum Scheduler {\n /// 单线程调度器\n CurrentThread(CurrentThread),\n /// 多线程调度器\n MultiThread(MultiThread),\n}\n```\n\n### Worker\n每一个Worker对应一个线程,其实也可以称Worker线程。\n```rust\npub(super) struct Worker {\n /// 调度器句柄\n handle: Arc<Handle>,\n /// Worker 索引\n index: usize,\n /// 核心数据结构\n core: AtomicCell<Core>,\n}\n\npub(super) struct Core {\n /// 本地任务队列\n run_queue: queue::Local,\n /// LIFO 槽位\n lifo_slot: Option<Notified>,\n /// 是否正在搜索任务\n is_searching: bool,\n /// 是否已关闭\n is_shutdown: bool,\n}\n```\n\n### 任务队列\n```rust\npub(super) struct Shared {\n /// 全局任务队列\n pub(super) inject: inject::Shared,\n /// 远程 Worker 列表\n pub(super) remotes: Box<[Remote]>,\n /// 空闲 Worker 管理\n pub(super) idle: Idle,\n /// 调度器配置\n pub(super) config: Config,\n}\n```\n\n## 工作窃取算法\n\n### 任务调度流程\n```rust\nimpl Worker {\n fn get_next_task(&mut self) -> Option<Notified> {\n // 1. 检查本地队列\n if let Some(task) = self.core.run_queue.pop() {\n return Some(task);\n }\n\n // 2. 检查全局注入队列\n if let Some(task) = self.handle.shared.inject.pop() {\n return Some(task);\n }\n\n // 3. 尝试从其他 Worker 窃取\n self.steal_work()\n }\n}\n```\n\n### 工作窃取实现\n```rust\nimpl Worker {\n fn steal_work(&self) -> Option<Notified> {\n let remotes = &self.handle.shared.remotes;\n \n for remote in remotes {\n if let Some(task) = remote.steal.steal_into(&mut self.core.run_queue) {\n return Some(task);\n }\n }\n \n None\n }\n}\n```\n\n## 任务调度优化\n\n### LIFO 优化\n```rust\nimpl Core {\n // 使用 LIFO 槽位优化最近提交的任务\n lifo_slot: Option<Notified>,\n}\n```\n\n### 批量处理\n```rust\nimpl Worker {\n fn steal_work(&self) -> Option<Notified> {\n let mut batch = Vec::new();\n if let Some(remote) = self.select_remote() {\n remote.steal.steal_into_batch(&mut batch);\n }\n batch.pop()\n }\n}\n```\n\n## 阻塞任务处理\n\n### 阻塞线程池\n```rust\npub(crate) struct BlockingPool {\n spawner: Spawner,\n shutdown_rx: shutdown::Receiver,\n}\n\n#[derive(Clone)]\npub(crate) struct Spawner {\n inner: Arc<Inner>,\n}\n```\n\n### 任务提交\n```rust\nimpl Spawner {\n pub(crate) fn spawn_blocking<F, R>(&self, rt: &Handle, func: F) -> JoinHandle<R>\n where\n F: FnOnce() -> R + Send + 'static,\n R: Send + 'static,\n {\n let (join_handle, spawn_result) = self.spawn_blocking_inner(\n func,\n Mandatory::NonMandatory,\n SpawnMeta::new_unnamed(fn_size),\n rt,\n );\n // ...\n }\n}\n```\n\n","source":"_posts/rokio源码实现简单分析.md","raw":"---\ntitle: rokio源码实现简单分析\ndate: 2025-06-02 16:41:48\ntags:\n - rust\n---\n\n待完成\n\n\n## 前言\n \nTokio 是一个 Rust 的异步运行时,它提供了一个完整的异步 I/O 框架。其实现在AI Code分析工具,比如Cursor、windsurf等基本都能分析出tokio核心实现,本文并不做八股文总结,仅尝试从任务调度实现角度分析,给出我们可以借鉴的设计思想,其中包括:\n- 工作窃取实现\n- 任务调度优化\n- 阻塞任务实现\n\n先来看tokio简单示例:\n- 创建运行时\n```rust\nlet rt = Runtime::builder()\n .worker_threads(4)\n .build()\n .unwrap();\n```\n\n- 提交任务\n```rust\nrt.spawn(async {\n println!(\"Running on worker thread\");\n});\n```\n\n- 提交阻塞任务\n```rust\nrt.spawn_blocking(|| {\n // 执行阻塞操作\n});\n```\n<!-- more -->\n## 整体架构\n作为异步 I/O 框架,其核心架构包含以下几个主要组件:\n### 核心组件\n```rust\npub struct Runtime {\n /// 调度器\n scheduler: Scheduler,\n /// 运行时句柄\n handle: Handle,\n /// 阻塞线程池\n blocking_pool: BlockingPool,\n}\n```\n\n### 调度器类型\n```rust\npub enum Scheduler {\n /// 单线程调度器\n CurrentThread(CurrentThread),\n /// 多线程调度器\n MultiThread(MultiThread),\n}\n```\n\n### Worker\n每一个Worker对应一个线程,其实也可以称Worker线程。\n```rust\npub(super) struct Worker {\n /// 调度器句柄\n handle: Arc<Handle>,\n /// Worker 索引\n index: usize,\n /// 核心数据结构\n core: AtomicCell<Core>,\n}\n\npub(super) struct Core {\n /// 本地任务队列\n run_queue: queue::Local,\n /// LIFO 槽位\n lifo_slot: Option<Notified>,\n /// 是否正在搜索任务\n is_searching: bool,\n /// 是否已关闭\n is_shutdown: bool,\n}\n```\n\n### 任务队列\n```rust\npub(super) struct Shared {\n /// 全局任务队列\n pub(super) inject: inject::Shared,\n /// 远程 Worker 列表\n pub(super) remotes: Box<[Remote]>,\n /// 空闲 Worker 管理\n pub(super) idle: Idle,\n /// 调度器配置\n pub(super) config: Config,\n}\n```\n\n## 工作窃取算法\n\n### 任务调度流程\n```rust\nimpl Worker {\n fn get_next_task(&mut self) -> Option<Notified> {\n // 1. 检查本地队列\n if let Some(task) = self.core.run_queue.pop() {\n return Some(task);\n }\n\n // 2. 检查全局注入队列\n if let Some(task) = self.handle.shared.inject.pop() {\n return Some(task);\n }\n\n // 3. 尝试从其他 Worker 窃取\n self.steal_work()\n }\n}\n```\n\n### 工作窃取实现\n```rust\nimpl Worker {\n fn steal_work(&self) -> Option<Notified> {\n let remotes = &self.handle.shared.remotes;\n \n for remote in remotes {\n if let Some(task) = remote.steal.steal_into(&mut self.core.run_queue) {\n return Some(task);\n }\n }\n \n None\n }\n}\n```\n\n## 任务调度优化\n\n### LIFO 优化\n```rust\nimpl Core {\n // 使用 LIFO 槽位优化最近提交的任务\n lifo_slot: Option<Notified>,\n}\n```\n\n### 批量处理\n```rust\nimpl Worker {\n fn steal_work(&self) -> Option<Notified> {\n let mut batch = Vec::new();\n if let Some(remote) = self.select_remote() {\n remote.steal.steal_into_batch(&mut batch);\n }\n batch.pop()\n }\n}\n```\n\n## 阻塞任务处理\n\n### 阻塞线程池\n```rust\npub(crate) struct BlockingPool {\n spawner: Spawner,\n shutdown_rx: shutdown::Receiver,\n}\n\n#[derive(Clone)]\npub(crate) struct Spawner {\n inner: Arc<Inner>,\n}\n```\n\n### 任务提交\n```rust\nimpl Spawner {\n pub(crate) fn spawn_blocking<F, R>(&self, rt: &Handle, func: F) -> JoinHandle<R>\n where\n F: FnOnce() -> R + Send + 'static,\n R: Send + 'static,\n {\n let (join_handle, spawn_result) = self.spawn_blocking_inner(\n func,\n Mandatory::NonMandatory,\n SpawnMeta::new_unnamed(fn_size),\n rt,\n );\n // ...\n }\n}\n```\n\n","slug":"rokio源码实现简单分析","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n870007catehlsohttr","content":"<p>待完成</p>\n<h2 id=\"前言\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言</h2><p><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/tokio_classes.png?raw=true\"><br>Tokio 是一个 Rust 的异步运行时,它提供了一个完整的异步 I/O 框架。其实现在AI Code分析工具,比如Cursor、windsurf等基本都能分析出tokio核心实现,本文并不做八股文总结,仅尝试从任务调度实现角度分析,给出我们可以借鉴的设计思想,其中包括:</p>\n<ul>\n<li>工作窃取实现</li>\n<li>任务调度优化</li>\n<li>阻塞任务实现</li>\n</ul>\n<p>先来看tokio简单示例:</p>\n<ul>\n<li>创建运行时</li>\n</ul>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">let</span> <span class=\"variable\">rt</span> = Runtime::<span class=\"title function_ invoke__\">builder</span>()</span><br><span class=\"line\"> .<span class=\"title function_ invoke__\">worker_threads</span>(<span class=\"number\">4</span>)</span><br><span class=\"line\"> .<span class=\"title function_ invoke__\">build</span>()</span><br><span class=\"line\"> .<span class=\"title function_ invoke__\">unwrap</span>();</span><br></pre></td></tr></table></figure>\n\n<ul>\n<li>提交任务</li>\n</ul>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">rt.<span class=\"title function_ invoke__\">spawn</span>(<span class=\"keyword\">async</span> {</span><br><span class=\"line\"> <span class=\"built_in\">println!</span>(<span class=\"string\">"Running on worker thread"</span>);</span><br><span class=\"line\">});</span><br></pre></td></tr></table></figure>\n\n<ul>\n<li>提交阻塞任务</li>\n</ul>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">rt.<span class=\"title function_ invoke__\">spawn_blocking</span>(|| {</span><br><span class=\"line\"> <span class=\"comment\">// 执行阻塞操作</span></span><br><span class=\"line\">});</span><br></pre></td></tr></table></figure>\n<span id=\"more\"></span>\n<h2 id=\"整体架构\"><a href=\"#整体架构\" class=\"headerlink\" title=\"整体架构\"></a>整体架构</h2><p>作为异步 I/O 框架,其核心架构包含以下几个主要组件:</p>\n<h3 id=\"核心组件\"><a href=\"#核心组件\" class=\"headerlink\" title=\"核心组件\"></a>核心组件</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">pub</span> <span class=\"keyword\">struct</span> <span class=\"title class_\">Runtime</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 调度器</span></span><br><span class=\"line\"> scheduler: Scheduler,</span><br><span class=\"line\"> <span class=\"comment\">/// 运行时句柄</span></span><br><span class=\"line\"> handle: Handle,</span><br><span class=\"line\"> <span class=\"comment\">/// 阻塞线程池</span></span><br><span class=\"line\"> blocking_pool: BlockingPool,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"调度器类型\"><a href=\"#调度器类型\" class=\"headerlink\" title=\"调度器类型\"></a>调度器类型</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">pub</span> <span class=\"keyword\">enum</span> <span class=\"title class_\">Scheduler</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 单线程调度器</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">CurrentThread</span>(CurrentThread),</span><br><span class=\"line\"> <span class=\"comment\">/// 多线程调度器</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">MultiThread</span>(MultiThread),</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"Worker\"><a href=\"#Worker\" class=\"headerlink\" title=\"Worker\"></a>Worker</h3><p>每一个Worker对应一个线程,其实也可以称Worker线程。</p>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 调度器句柄</span></span><br><span class=\"line\"> handle: Arc<Handle>,</span><br><span class=\"line\"> <span class=\"comment\">/// Worker 索引</span></span><br><span class=\"line\"> index: <span class=\"type\">usize</span>,</span><br><span class=\"line\"> <span class=\"comment\">/// 核心数据结构</span></span><br><span class=\"line\"> core: AtomicCell<Core>,</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Core</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 本地任务队列</span></span><br><span class=\"line\"> run_queue: queue::Local,</span><br><span class=\"line\"> <span class=\"comment\">/// LIFO 槽位</span></span><br><span class=\"line\"> lifo_slot: <span class=\"type\">Option</span><Notified>,</span><br><span class=\"line\"> <span class=\"comment\">/// 是否正在搜索任务</span></span><br><span class=\"line\"> is_searching: <span class=\"type\">bool</span>,</span><br><span class=\"line\"> <span class=\"comment\">/// 是否已关闭</span></span><br><span class=\"line\"> is_shutdown: <span class=\"type\">bool</span>,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"任务队列\"><a href=\"#任务队列\" class=\"headerlink\" title=\"任务队列\"></a>任务队列</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Shared</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 全局任务队列</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) inject: inject::Shared,</span><br><span class=\"line\"> <span class=\"comment\">/// 远程 Worker 列表</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) remotes: <span class=\"type\">Box</span><[Remote]>,</span><br><span class=\"line\"> <span class=\"comment\">/// 空闲 Worker 管理</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) idle: Idle,</span><br><span class=\"line\"> <span class=\"comment\">/// 调度器配置</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) config: Config,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"工作窃取算法\"><a href=\"#工作窃取算法\" class=\"headerlink\" title=\"工作窃取算法\"></a>工作窃取算法</h2><h3 id=\"任务调度流程\"><a href=\"#任务调度流程\" class=\"headerlink\" title=\"任务调度流程\"></a>任务调度流程</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"keyword\">fn</span> <span class=\"title function_\">get_next_task</span>(&<span class=\"keyword\">mut</span> <span class=\"keyword\">self</span>) <span class=\"punctuation\">-></span> <span class=\"type\">Option</span><Notified> {</span><br><span class=\"line\"> <span class=\"comment\">// 1. 检查本地队列</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(task) = <span class=\"keyword\">self</span>.core.run_queue.<span class=\"title function_ invoke__\">pop</span>() {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"title function_ invoke__\">Some</span>(task);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 2. 检查全局注入队列</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(task) = <span class=\"keyword\">self</span>.handle.shared.inject.<span class=\"title function_ invoke__\">pop</span>() {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"title function_ invoke__\">Some</span>(task);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 3. 尝试从其他 Worker 窃取</span></span><br><span class=\"line\"> <span class=\"keyword\">self</span>.<span class=\"title function_ invoke__\">steal_work</span>()</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"工作窃取实现\"><a href=\"#工作窃取实现\" class=\"headerlink\" title=\"工作窃取实现\"></a>工作窃取实现</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"keyword\">fn</span> <span class=\"title function_\">steal_work</span>(&<span class=\"keyword\">self</span>) <span class=\"punctuation\">-></span> <span class=\"type\">Option</span><Notified> {</span><br><span class=\"line\"> <span class=\"keyword\">let</span> <span class=\"variable\">remotes</span> = &<span class=\"keyword\">self</span>.handle.shared.remotes;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">for</span> <span class=\"variable\">remote</span> <span class=\"keyword\">in</span> remotes {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(task) = remote.steal.<span class=\"title function_ invoke__\">steal_into</span>(&<span class=\"keyword\">mut</span> <span class=\"keyword\">self</span>.core.run_queue) {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"title function_ invoke__\">Some</span>(task);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"literal\">None</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"任务调度优化\"><a href=\"#任务调度优化\" class=\"headerlink\" title=\"任务调度优化\"></a>任务调度优化</h2><h3 id=\"LIFO-优化\"><a href=\"#LIFO-优化\" class=\"headerlink\" title=\"LIFO 优化\"></a>LIFO 优化</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Core</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 使用 LIFO 槽位优化最近提交的任务</span></span><br><span class=\"line\"> lifo_slot: <span class=\"type\">Option</span><Notified>,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"批量处理\"><a href=\"#批量处理\" class=\"headerlink\" title=\"批量处理\"></a>批量处理</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"keyword\">fn</span> <span class=\"title function_\">steal_work</span>(&<span class=\"keyword\">self</span>) <span class=\"punctuation\">-></span> <span class=\"type\">Option</span><Notified> {</span><br><span class=\"line\"> <span class=\"keyword\">let</span> <span class=\"keyword\">mut </span><span class=\"variable\">batch</span> = <span class=\"type\">Vec</span>::<span class=\"title function_ invoke__\">new</span>();</span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(remote) = <span class=\"keyword\">self</span>.<span class=\"title function_ invoke__\">select_remote</span>() {</span><br><span class=\"line\"> remote.steal.<span class=\"title function_ invoke__\">steal_into_batch</span>(&<span class=\"keyword\">mut</span> batch);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> batch.<span class=\"title function_ invoke__\">pop</span>()</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"阻塞任务处理\"><a href=\"#阻塞任务处理\" class=\"headerlink\" title=\"阻塞任务处理\"></a>阻塞任务处理</h2><h3 id=\"阻塞线程池\"><a href=\"#阻塞线程池\" class=\"headerlink\" title=\"阻塞线程池\"></a>阻塞线程池</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">crate</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">BlockingPool</span> {</span><br><span class=\"line\"> spawner: Spawner,</span><br><span class=\"line\"> shutdown_rx: shutdown::Receiver,</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#[derive(Clone)]</span></span><br><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">crate</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Spawner</span> {</span><br><span class=\"line\"> inner: Arc<Inner>,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"任务提交\"><a href=\"#任务提交\" class=\"headerlink\" title=\"任务提交\"></a>任务提交</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Spawner</span> {</span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">crate</span>) <span class=\"keyword\">fn</span> <span class=\"title function_\">spawn_blocking</span><F, R>(&<span class=\"keyword\">self</span>, rt: &Handle, func: F) <span class=\"punctuation\">-></span> JoinHandle<R></span><br><span class=\"line\"> <span class=\"keyword\">where</span></span><br><span class=\"line\"> F: <span class=\"title function_ invoke__\">FnOnce</span>() <span class=\"punctuation\">-></span> R + <span class=\"built_in\">Send</span> + <span class=\"symbol\">'static</span>,</span><br><span class=\"line\"> R: <span class=\"built_in\">Send</span> + <span class=\"symbol\">'static</span>,</span><br><span class=\"line\"> {</span><br><span class=\"line\"> <span class=\"keyword\">let</span> (join_handle, spawn_result) = <span class=\"keyword\">self</span>.<span class=\"title function_ invoke__\">spawn_blocking_inner</span>(</span><br><span class=\"line\"> func,</span><br><span class=\"line\"> Mandatory::NonMandatory,</span><br><span class=\"line\"> SpawnMeta::<span class=\"title function_ invoke__\">new_unnamed</span>(fn_size),</span><br><span class=\"line\"> rt,</span><br><span class=\"line\"> );</span><br><span class=\"line\"> <span class=\"comment\">// ...</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n","excerpt":"<p>待完成</p>\n<h2 id=\"前言\"><a href=\"#前言\" class=\"headerlink\" title=\"前言\"></a>前言</h2><p><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/tokio_classes.png?raw=true\"><br>Tokio 是一个 Rust 的异步运行时,它提供了一个完整的异步 I/O 框架。其实现在AI Code分析工具,比如Cursor、windsurf等基本都能分析出tokio核心实现,本文并不做八股文总结,仅尝试从任务调度实现角度分析,给出我们可以借鉴的设计思想,其中包括:</p>\n<ul>\n<li>工作窃取实现</li>\n<li>任务调度优化</li>\n<li>阻塞任务实现</li>\n</ul>\n<p>先来看tokio简单示例:</p>\n<ul>\n<li>创建运行时</li>\n</ul>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">let</span> <span class=\"variable\">rt</span> = Runtime::<span class=\"title function_ invoke__\">builder</span>()</span><br><span class=\"line\"> .<span class=\"title function_ invoke__\">worker_threads</span>(<span class=\"number\">4</span>)</span><br><span class=\"line\"> .<span class=\"title function_ invoke__\">build</span>()</span><br><span class=\"line\"> .<span class=\"title function_ invoke__\">unwrap</span>();</span><br></pre></td></tr></table></figure>\n\n<ul>\n<li>提交任务</li>\n</ul>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">rt.<span class=\"title function_ invoke__\">spawn</span>(<span class=\"keyword\">async</span> {</span><br><span class=\"line\"> <span class=\"built_in\">println!</span>(<span class=\"string\">"Running on worker thread"</span>);</span><br><span class=\"line\">});</span><br></pre></td></tr></table></figure>\n\n<ul>\n<li>提交阻塞任务</li>\n</ul>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">rt.<span class=\"title function_ invoke__\">spawn_blocking</span>(|| {</span><br><span class=\"line\"> <span class=\"comment\">// 执行阻塞操作</span></span><br><span class=\"line\">});</span><br></pre></td></tr></table></figure>","more":"<h2 id=\"整体架构\"><a href=\"#整体架构\" class=\"headerlink\" title=\"整体架构\"></a>整体架构</h2><p>作为异步 I/O 框架,其核心架构包含以下几个主要组件:</p>\n<h3 id=\"核心组件\"><a href=\"#核心组件\" class=\"headerlink\" title=\"核心组件\"></a>核心组件</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">pub</span> <span class=\"keyword\">struct</span> <span class=\"title class_\">Runtime</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 调度器</span></span><br><span class=\"line\"> scheduler: Scheduler,</span><br><span class=\"line\"> <span class=\"comment\">/// 运行时句柄</span></span><br><span class=\"line\"> handle: Handle,</span><br><span class=\"line\"> <span class=\"comment\">/// 阻塞线程池</span></span><br><span class=\"line\"> blocking_pool: BlockingPool,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"调度器类型\"><a href=\"#调度器类型\" class=\"headerlink\" title=\"调度器类型\"></a>调度器类型</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">pub</span> <span class=\"keyword\">enum</span> <span class=\"title class_\">Scheduler</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 单线程调度器</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">CurrentThread</span>(CurrentThread),</span><br><span class=\"line\"> <span class=\"comment\">/// 多线程调度器</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">MultiThread</span>(MultiThread),</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"Worker\"><a href=\"#Worker\" class=\"headerlink\" title=\"Worker\"></a>Worker</h3><p>每一个Worker对应一个线程,其实也可以称Worker线程。</p>\n<figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 调度器句柄</span></span><br><span class=\"line\"> handle: Arc<Handle>,</span><br><span class=\"line\"> <span class=\"comment\">/// Worker 索引</span></span><br><span class=\"line\"> index: <span class=\"type\">usize</span>,</span><br><span class=\"line\"> <span class=\"comment\">/// 核心数据结构</span></span><br><span class=\"line\"> core: AtomicCell<Core>,</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Core</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 本地任务队列</span></span><br><span class=\"line\"> run_queue: queue::Local,</span><br><span class=\"line\"> <span class=\"comment\">/// LIFO 槽位</span></span><br><span class=\"line\"> lifo_slot: <span class=\"type\">Option</span><Notified>,</span><br><span class=\"line\"> <span class=\"comment\">/// 是否正在搜索任务</span></span><br><span class=\"line\"> is_searching: <span class=\"type\">bool</span>,</span><br><span class=\"line\"> <span class=\"comment\">/// 是否已关闭</span></span><br><span class=\"line\"> is_shutdown: <span class=\"type\">bool</span>,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"任务队列\"><a href=\"#任务队列\" class=\"headerlink\" title=\"任务队列\"></a>任务队列</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Shared</span> {</span><br><span class=\"line\"> <span class=\"comment\">/// 全局任务队列</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) inject: inject::Shared,</span><br><span class=\"line\"> <span class=\"comment\">/// 远程 Worker 列表</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) remotes: <span class=\"type\">Box</span><[Remote]>,</span><br><span class=\"line\"> <span class=\"comment\">/// 空闲 Worker 管理</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) idle: Idle,</span><br><span class=\"line\"> <span class=\"comment\">/// 调度器配置</span></span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">super</span>) config: Config,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"工作窃取算法\"><a href=\"#工作窃取算法\" class=\"headerlink\" title=\"工作窃取算法\"></a>工作窃取算法</h2><h3 id=\"任务调度流程\"><a href=\"#任务调度流程\" class=\"headerlink\" title=\"任务调度流程\"></a>任务调度流程</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"keyword\">fn</span> <span class=\"title function_\">get_next_task</span>(&<span class=\"keyword\">mut</span> <span class=\"keyword\">self</span>) <span class=\"punctuation\">-></span> <span class=\"type\">Option</span><Notified> {</span><br><span class=\"line\"> <span class=\"comment\">// 1. 检查本地队列</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(task) = <span class=\"keyword\">self</span>.core.run_queue.<span class=\"title function_ invoke__\">pop</span>() {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"title function_ invoke__\">Some</span>(task);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 2. 检查全局注入队列</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(task) = <span class=\"keyword\">self</span>.handle.shared.inject.<span class=\"title function_ invoke__\">pop</span>() {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"title function_ invoke__\">Some</span>(task);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 3. 尝试从其他 Worker 窃取</span></span><br><span class=\"line\"> <span class=\"keyword\">self</span>.<span class=\"title function_ invoke__\">steal_work</span>()</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"工作窃取实现\"><a href=\"#工作窃取实现\" class=\"headerlink\" title=\"工作窃取实现\"></a>工作窃取实现</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"keyword\">fn</span> <span class=\"title function_\">steal_work</span>(&<span class=\"keyword\">self</span>) <span class=\"punctuation\">-></span> <span class=\"type\">Option</span><Notified> {</span><br><span class=\"line\"> <span class=\"keyword\">let</span> <span class=\"variable\">remotes</span> = &<span class=\"keyword\">self</span>.handle.shared.remotes;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">for</span> <span class=\"variable\">remote</span> <span class=\"keyword\">in</span> remotes {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(task) = remote.steal.<span class=\"title function_ invoke__\">steal_into</span>(&<span class=\"keyword\">mut</span> <span class=\"keyword\">self</span>.core.run_queue) {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"title function_ invoke__\">Some</span>(task);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"literal\">None</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"任务调度优化\"><a href=\"#任务调度优化\" class=\"headerlink\" title=\"任务调度优化\"></a>任务调度优化</h2><h3 id=\"LIFO-优化\"><a href=\"#LIFO-优化\" class=\"headerlink\" title=\"LIFO 优化\"></a>LIFO 优化</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Core</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 使用 LIFO 槽位优化最近提交的任务</span></span><br><span class=\"line\"> lifo_slot: <span class=\"type\">Option</span><Notified>,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"批量处理\"><a href=\"#批量处理\" class=\"headerlink\" title=\"批量处理\"></a>批量处理</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Worker</span> {</span><br><span class=\"line\"> <span class=\"keyword\">fn</span> <span class=\"title function_\">steal_work</span>(&<span class=\"keyword\">self</span>) <span class=\"punctuation\">-></span> <span class=\"type\">Option</span><Notified> {</span><br><span class=\"line\"> <span class=\"keyword\">let</span> <span class=\"keyword\">mut </span><span class=\"variable\">batch</span> = <span class=\"type\">Vec</span>::<span class=\"title function_ invoke__\">new</span>();</span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">let</span> <span class=\"variable\">Some</span>(remote) = <span class=\"keyword\">self</span>.<span class=\"title function_ invoke__\">select_remote</span>() {</span><br><span class=\"line\"> remote.steal.<span class=\"title function_ invoke__\">steal_into_batch</span>(&<span class=\"keyword\">mut</span> batch);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> batch.<span class=\"title function_ invoke__\">pop</span>()</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"阻塞任务处理\"><a href=\"#阻塞任务处理\" class=\"headerlink\" title=\"阻塞任务处理\"></a>阻塞任务处理</h2><h3 id=\"阻塞线程池\"><a href=\"#阻塞线程池\" class=\"headerlink\" title=\"阻塞线程池\"></a>阻塞线程池</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">crate</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">BlockingPool</span> {</span><br><span class=\"line\"> spawner: Spawner,</span><br><span class=\"line\"> shutdown_rx: shutdown::Receiver,</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#[derive(Clone)]</span></span><br><span class=\"line\"><span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">crate</span>) <span class=\"keyword\">struct</span> <span class=\"title class_\">Spawner</span> {</span><br><span class=\"line\"> inner: Arc<Inner>,</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"任务提交\"><a href=\"#任务提交\" class=\"headerlink\" title=\"任务提交\"></a>任务提交</h3><figure class=\"highlight rust\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">impl</span> <span class=\"title class_\">Spawner</span> {</span><br><span class=\"line\"> <span class=\"title function_ invoke__\">pub</span>(<span class=\"keyword\">crate</span>) <span class=\"keyword\">fn</span> <span class=\"title function_\">spawn_blocking</span><F, R>(&<span class=\"keyword\">self</span>, rt: &Handle, func: F) <span class=\"punctuation\">-></span> JoinHandle<R></span><br><span class=\"line\"> <span class=\"keyword\">where</span></span><br><span class=\"line\"> F: <span class=\"title function_ invoke__\">FnOnce</span>() <span class=\"punctuation\">-></span> R + <span class=\"built_in\">Send</span> + <span class=\"symbol\">'static</span>,</span><br><span class=\"line\"> R: <span class=\"built_in\">Send</span> + <span class=\"symbol\">'static</span>,</span><br><span class=\"line\"> {</span><br><span class=\"line\"> <span class=\"keyword\">let</span> (join_handle, spawn_result) = <span class=\"keyword\">self</span>.<span class=\"title function_ invoke__\">spawn_blocking_inner</span>(</span><br><span class=\"line\"> func,</span><br><span class=\"line\"> Mandatory::NonMandatory,</span><br><span class=\"line\"> SpawnMeta::<span class=\"title function_ invoke__\">new_unnamed</span>(fn_size),</span><br><span class=\"line\"> rt,</span><br><span class=\"line\"> );</span><br><span class=\"line\"> <span class=\"comment\">// ...</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>"},{"title":"一种有效管控APP隐私权限的解决方案","date":"2019-11-16T09:20:14.000Z","_content":"# 引言\n诸如读写外置存储、读取联系人、发短信等隐私权限,android在6.0系统开始进行[动态授权](https://developer.android.com/guide/topics/permissions/overview)。但在我国,仅向用户提示授权框还不够,工信部在19年11月初发布了[专项整治App八类侵权行为审明](http://www.xinhuanet.com/2019-11/05/c_1125192397.htm) ,其文明确治理以下八类问题:\n>1.私自收集个人信息; \n>2.超范围收集个人信息; \n>3.私自共享给第三方用户信息; \n>4.强制用户使用定向推送功能; \n>5.不给权限不让用; \n>6.频繁申请权限; \n>7.过度索取权限; \n>8.为用户账号注销设置障碍。 \n\n很不幸,网报通告批评:我司老版本APP中审明了隐私权限,但在隐私文档中并未进行有效说明。收到通告,团队立马对权限进行了扫描,发现APP在AndroidManifest中审明了三项隐私权限,但实际过程并未使用(有些冤大头)。我相信很多团队跟我们面临同个问题,多团队开发下,权限引入问题没有一个有效监管机制。为避免类似问题再次发生,本文给出一个简单有效的代码编译层拦截方案。 \n\n在说方案原理之前,我们先假定检测方案是扫描APP AndroidManifest.xml文件中审明的和用户有关的隐私权限,再比对隐私文档以及实际使用场景,进行判别。面对检测方案,我们给出解决思路:\n>在编译阶段processApplicationManifest task运行后,对Merged Manifest Log文件进行扫描,如果用到了新权限,抛出打包错误,直至问题解决;\n\n<!-- more -->\n# 源码简阅\n\nAndroid Gradle Plugin在编译APP后,会在build/outputs/logs目录下生成名为【manifest-merger-${variantname}-report.txt】文本文件。\n以AGP 3.5.0源码为例,简单分析下ProcessApplicationManifest任务是如何产生Merged Manifest Log文件的。\n``` \npackage com.android.build.gradle.tasks;\n\n/** A task that processes the manifest */\n@CacheableTask\npublic abstract class ProcessApplicationManifest extends ManifestProcessorTask {\n @Override\n @Internal\n protected boolean getIncremental() {\n return true;\n }\n\n @Override\n protected void doFullTaskAction() throws IOException {\n ... ...\n MergingReport mergingReport =\n ManifestHelperKt.mergeManifestsForApplication(\n getMainManifest(),\n getManifestOverlays(),\n computeFullProviderList(compatibleScreenManifestForSplit),\n navigationXmls,\n getFeatureName(),\n moduleMetadata == null\n ? getPackageOverride()\n : moduleMetadata.getApplicationId(),\n moduleMetadata == null\n ? apkData.getVersionCode()\n : Integer.parseInt(moduleMetadata.getVersionCode()),\n moduleMetadata == null\n ? apkData.getVersionName()\n : moduleMetadata.getVersionName(),\n getMinSdkVersion(),\n getTargetSdkVersion(),\n getMaxSdkVersion(),\n manifestOutputFile.getAbsolutePath(),\n // no aapt friendly merged manifest file necessary for applications.\n null /* aaptFriendlyManifestOutputFile */,\n metadataFeatureManifestOutputFile.getAbsolutePath(),\n bundleManifestOutputFile.getAbsolutePath(),\n instantAppManifestOutputFile != null\n ? instantAppManifestOutputFile.getAbsolutePath()\n : null,\n ManifestMerger2.MergeType.APPLICATION,\n variantConfiguration.getManifestPlaceholders(),\n getOptionalFeatures(),\n getReportFile(), \n LoggerWrapper.getLogger(ProcessApplicationManifest.class));\n ... ...\n }\n\n public static class CreationAction\n extends AnnotationProcessingTaskCreationAction<ProcessApplicationManifest> {\n\n private File reportFile;\n\n @Override\n public void preConfigure(@NonNull String taskName) {\n super.preConfigure(taskName);\n \n //这里就【manifest-merger-${variantname}-report.txt】文件\n reportFile =\n FileUtils.join(\n variantScope.getGlobalScope().getOutputsDir(),\n \"logs\",\n \"manifest-merger-\"\n + variantScope.getVariantConfiguration().getBaseName()\n + \"-report.txt\");\n }\n } \n}\n```\n\n通过代码,可以发现ProcessApplicationManifest是交给ManifestHelperKt.mergeManifestsForApplication方法对所有Manifest进行合并处理的,并且Log保存在【manifest-merger-${variantname}-report.txt】文件中。\n```\npackage com.android.build.gradle.internal.tasks.manifest\n\n/** Invoke the Manifest Merger version 2. */\nfun mergeManifestsForApplication(\n mainManifest: File,\n manifestOverlays: List<File>,\n dependencies: List<ManifestProvider>,\n navigationFiles: List<File>,\n featureName: String?,\n packageOverride: String?,\n versionCode: Int,\n versionName: String?,\n minSdkVersion: String?,\n targetSdkVersion: String?,\n maxSdkVersion: Int?,\n outManifestLocation: String,\n outAaptSafeManifestLocation: String?,\n outMetadataFeatureManifestLocation: String?,\n outBundleManifestLocation: String?,\n outInstantAppManifestLocation: String?,\n mergeType: ManifestMerger2.MergeType,\n placeHolders: Map<String, Any>,\n optionalFeatures: Collection<ManifestMerger2.Invoker.Feature>,\n reportFile: File?,\n logger: ILogger\n): MergingReport {\n\n try {\n\n //ManifestMerger2是 manifest-merger库提供的辅助类\n val manifestMergerInvoker = ManifestMerger2.newMerger(mainManifest, logger, mergeType)\n .setPlaceHolderValues(placeHolders)\n .addFlavorAndBuildTypeManifests(*manifestOverlays.toTypedArray())\n .addManifestProviders(dependencies)\n .addNavigationFiles(navigationFiles)\n .withFeatures(*optionalFeatures.toTypedArray())\n .setMergeReportFile(reportFile)\n .setFeatureName(featureName)\n\n if (mergeType == ManifestMerger2.MergeType.APPLICATION) {\n manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)\n }\n\n\n if (outAaptSafeManifestLocation != null) {\n manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.MAKE_AAPT_SAFE)\n }\n\n setInjectableValues(\n manifestMergerInvoker,\n packageOverride, versionCode, versionName,\n minSdkVersion, targetSdkVersion, maxSdkVersion\n )\n \n //关注这里的调用\n val mergingReport = manifestMergerInvoker.merge()\n //省略其他对merge结果处理代码\n ... ...\n return mergingReport\n } catch (e: ManifestMerger2.MergeFailureException) {\n // TODO: unacceptable.\n throw RuntimeException(e)\n }\n}\n```\n\n接着看manifestMergerInvoker.merge()的实现\n```\npackage com.android.manifmerger;\n\n/**\n * merges android manifest files, idempotent.\n */\n@Immutable\npublic class ManifestMerger2 {\n public static class Invoker<T extends Invoker<T>>{\n\n @NonNull\n public MergingReport merge() throws MergeFailureException {\n\n // provide some free placeholders values.\n ImmutableMap<ManifestSystemProperty, Object> systemProperties = mSystemProperties.build();\n ... ...\n FileStreamProvider fileStreamProvider = mFileStreamProvider != null\n ? mFileStreamProvider : new FileStreamProvider();\n ManifestMerger2 manifestMerger =\n new ManifestMerger2(\n mLogger,\n mMainManifestFile,\n mLibraryFilesBuilder.build(),\n mFlavorsAndBuildTypeFiles.build(),\n mFeaturesBuilder.build(),\n mPlaceholders.build(),\n new MapBasedKeyBasedValueResolver<ManifestSystemProperty>(\n systemProperties),\n mMergeType,\n mDocumentType,\n Optional.fromNullable(mReportFile),\n mFeatureName,\n fileStreamProvider,\n mNavigationFilesBuilder.build());\n //调用下面的 private MergingReport merge()方法 \n return manifestMerger.merge();\n }\n }\n\n\n /**\n * Perform high level ordering of files merging and delegates actual merging to\n * {@link XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)}\n *\n * @return the merging activity report.\n * @throws MergeFailureException if the merging cannot be completed (for instance, if xml\n * files cannot be loaded).\n */\n @NonNull\n private MergingReport merge() throws MergeFailureException {\n // initiate a new merging report\n MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger);\n //一系列merge manifest规则处理\n ... ...\n MergingReport mergingReport = mergingReportBuilder.build();\n\n if (mReportFile.isPresent()) {\n writeReport(mergingReport);\n }\n return mergingReport;\n }\n\n //最终写入Log文件方法\n /**\n * Creates the merging report file.\n * @param mergingReport the merging activities report to serialize.\n */\n private void writeReport(@NonNull MergingReport mergingReport) {\n FileWriter fileWriter = null;\n ... ... \n fileWriter = new FileWriter(mReportFile.get());\n mergingReport.getActions().log(fileWriter);\n } \n} \n```\n\n到目前为止,从代码层面看到了Log文件是如何生成的。\n# 方案实现\n\n【manifest-merger-${variantname}-report.txt】文件大致内容如下:\n```\n-- Merging decision tree log ---\nmanifest\nADDED from /somepath/AndroidManifest.xml:x:x-xx:xx\nMERGED from [dependencies sdk] /somepath/AndroidManifest.xml:x:x-xx:xx\nINJECTED from /somepath/AndroidManifest.xml:x:x-xx:xx\n...\nuses-permission#android.permission.INTERNET\n```\n\n方案代码实现很简单:\n>1.自定义一个Extension,列出暂禁用的权限;\n>2.实现相应Plugin和Task;\n\nExtension定义可以如下所示:\n```\nhost{\n //明确暂禁用的权限列表\n forbiddenPermissions = ['android.permission.GET_ACCOUNTS',\n 'android.permission.SEND_SMS',\n 'android.permission.CALL_PHONE',\n 'android.permission.BLUETOOTH',\n ... ...] \n}\n```\n\nPlugin简单示例:\n```\npublic class HostPlugin implements Plugin<Project> {\n @Override\n final void apply(Project project) {\n if (!project.getPlugins().hasPlugin('com.android.application') && !project.getPlugins().hasPlugin('com.android.library')) {\n throw new GradleException('apply plugin: \\'com.android.application\\' or apply plugin: \\'com.android.library\\' is required')\n }\n HostExtension hostExtension = project.getExtensions().create('host', HostExtension.class)\n \n project.afterEvaluate {\n def variants = null;\n if (project.plugins.hasPlugin('com.android.application')) {\n variants = android.getApplicationVariants()\n } else if (project.plugins.hasPlugin('com.android.library')) {\n variants = android.getLibraryVariants()\n }\n variants?.all { BaseVariant variant ->\n MergeHostManifestTask taskConfiguration= new MergeHostManifestTask.CreationAction()\n project.getTasks().create(taskConfiguration.getName(), taskConfiguration.getType(), taskConfiguration)\n }\n }\n } \n}\n```\n\nTask简单示例:\n```\nimport org.gradle.util.GFileUtils\nimport com.android.utils.FileUtils\n\nclass MergeHostManifestTask extends DefaultTask {\n\n List<String> forbiddenPermissions //禁用的权限列表\n\n VariantScope scope\n\n @TaskAction\n def doFullTaskAction() {\n\n File logFile = FileUtils.join(\n scope.getGlobalScope().getOutputsDir(),\n \"logs\",\n \"manifest-permissions-validate-\"\n + scope.getVariantConfiguration().getBaseName()\n + \"-report.txt\")\n GFileUtils.mkdirs(logFile.getParentFile())\n GFileUtils.deleteQuietly(logFile) \n\n checkHostManifest(forbiddenPermissions,logFile,scope)\n if (logFile.exists() && logFile.length() > 0) {\n throw new GradleException(\"Has forbidden permissions in host, please check it in file ${logFile.getAbsolutePath()}\")\n } \n } \n\n /**\n * 检测host manifest 是否含有禁用权限列表\n * @param forbiddenPermissions\n * @param logFile\n * @param variantScope\n */\n public static void checkHostManifest(List<String> forbiddenPermissions, File logFile, def variantScope) {\n if (forbiddenPermissions == null || forbiddenPermissions.isEmpty()) {\n return\n }\n\n File reportFile =\n FileUtils.join(\n variantScope.getGlobalScope().getOutputsDir(),\n \"logs\",\n \"manifest-merger-\"\n + variantScope.getVariantConfiguration().getBaseName()\n + \"-report.txt\")\n\n if (!reportFile.exists()) {\n return\n }\n\n reportFile.withReader { reader ->\n String line\n while ((line = reader.readLine()) != null) {\n forbiddenPermissions.each { p ->\n if (line.contains(\"uses-permission#${p.trim()}\")) {\n logFile.append(\"${p.trim()}\\n\")\n logFile.append(reader.readLine())\n logFile.append(\"\\n\")\n }\n }\n }\n }\n }\n\n public static class CreationAction\n extends TaskConfiguration<MergeHostManifestTask> {\n\n BaseVariant variant\n\n Project project\n\n public CreationAction(Project project,BaseVariant variant){\n this.project= project\n this.variant=variant\n }\n\n @Override\n void execute(MergeHostManifestTask task) {\n ... ...\n HostExtension hostExtension = project.getExtensions().findByType(HostExtension.class)\n task.forbiddenPermissions = hostExtension.getForbiddenPermissions()\n task.scope= variant.getMetaClass().getProperty(variant, 'variantData').getScope()\n task.dependsOn getProcessManifestTask()\n }\n\n private Task getProcessManifestTaskCompat() {\n try {\n //>=3.3.0\n String taskName = variant.getMetaClass().getProperty(variant, 'variantData').getScope().getTaskContainer().getProcessManifestTask().getName()\n return project.getTasks().findByName(taskName)\n } catch (Exception e) {\n\n }\n }\n} \n```\n\n如果APP或其依赖的SDK,有引入禁用权限,则会抛出编译异常,生成的【manifest-permissions-validate-${variantname}-report.txt】文件内容类似以下所示:\n```\nandroid.permission.SEND_SMS\nADDED from /../app/src/main/AndroidManifest.xml:9:5-67\nandroid.permission.BLUETOOTH\nADDED from /../app/src/main/AndroidManifest.xml:11:5-68\n```\n\n# 结束语\n关于隐私权限列表,相关部门也未给允一个完整的列表,建议团队把所有未在隐私文档中描述的动态权限都作为禁用权限,直至隐私文档同步。\n\n# 参考\n1.Android Gradle Plugin:[https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core) \n","source":"_posts/一种有效管控APP隐私权限的解决方案.md","raw":"---\ntitle: 一种有效管控APP隐私权限的解决方案\ndate: 2019-11-16 17:20:14\ncategories: \n - Android\ntags: \n - 小功能组\n---\n# 引言\n诸如读写外置存储、读取联系人、发短信等隐私权限,android在6.0系统开始进行[动态授权](https://developer.android.com/guide/topics/permissions/overview)。但在我国,仅向用户提示授权框还不够,工信部在19年11月初发布了[专项整治App八类侵权行为审明](http://www.xinhuanet.com/2019-11/05/c_1125192397.htm) ,其文明确治理以下八类问题:\n>1.私自收集个人信息; \n>2.超范围收集个人信息; \n>3.私自共享给第三方用户信息; \n>4.强制用户使用定向推送功能; \n>5.不给权限不让用; \n>6.频繁申请权限; \n>7.过度索取权限; \n>8.为用户账号注销设置障碍。 \n\n很不幸,网报通告批评:我司老版本APP中审明了隐私权限,但在隐私文档中并未进行有效说明。收到通告,团队立马对权限进行了扫描,发现APP在AndroidManifest中审明了三项隐私权限,但实际过程并未使用(有些冤大头)。我相信很多团队跟我们面临同个问题,多团队开发下,权限引入问题没有一个有效监管机制。为避免类似问题再次发生,本文给出一个简单有效的代码编译层拦截方案。 \n\n在说方案原理之前,我们先假定检测方案是扫描APP AndroidManifest.xml文件中审明的和用户有关的隐私权限,再比对隐私文档以及实际使用场景,进行判别。面对检测方案,我们给出解决思路:\n>在编译阶段processApplicationManifest task运行后,对Merged Manifest Log文件进行扫描,如果用到了新权限,抛出打包错误,直至问题解决;\n\n<!-- more -->\n# 源码简阅\n\nAndroid Gradle Plugin在编译APP后,会在build/outputs/logs目录下生成名为【manifest-merger-${variantname}-report.txt】文本文件。\n以AGP 3.5.0源码为例,简单分析下ProcessApplicationManifest任务是如何产生Merged Manifest Log文件的。\n``` \npackage com.android.build.gradle.tasks;\n\n/** A task that processes the manifest */\n@CacheableTask\npublic abstract class ProcessApplicationManifest extends ManifestProcessorTask {\n @Override\n @Internal\n protected boolean getIncremental() {\n return true;\n }\n\n @Override\n protected void doFullTaskAction() throws IOException {\n ... ...\n MergingReport mergingReport =\n ManifestHelperKt.mergeManifestsForApplication(\n getMainManifest(),\n getManifestOverlays(),\n computeFullProviderList(compatibleScreenManifestForSplit),\n navigationXmls,\n getFeatureName(),\n moduleMetadata == null\n ? getPackageOverride()\n : moduleMetadata.getApplicationId(),\n moduleMetadata == null\n ? apkData.getVersionCode()\n : Integer.parseInt(moduleMetadata.getVersionCode()),\n moduleMetadata == null\n ? apkData.getVersionName()\n : moduleMetadata.getVersionName(),\n getMinSdkVersion(),\n getTargetSdkVersion(),\n getMaxSdkVersion(),\n manifestOutputFile.getAbsolutePath(),\n // no aapt friendly merged manifest file necessary for applications.\n null /* aaptFriendlyManifestOutputFile */,\n metadataFeatureManifestOutputFile.getAbsolutePath(),\n bundleManifestOutputFile.getAbsolutePath(),\n instantAppManifestOutputFile != null\n ? instantAppManifestOutputFile.getAbsolutePath()\n : null,\n ManifestMerger2.MergeType.APPLICATION,\n variantConfiguration.getManifestPlaceholders(),\n getOptionalFeatures(),\n getReportFile(), \n LoggerWrapper.getLogger(ProcessApplicationManifest.class));\n ... ...\n }\n\n public static class CreationAction\n extends AnnotationProcessingTaskCreationAction<ProcessApplicationManifest> {\n\n private File reportFile;\n\n @Override\n public void preConfigure(@NonNull String taskName) {\n super.preConfigure(taskName);\n \n //这里就【manifest-merger-${variantname}-report.txt】文件\n reportFile =\n FileUtils.join(\n variantScope.getGlobalScope().getOutputsDir(),\n \"logs\",\n \"manifest-merger-\"\n + variantScope.getVariantConfiguration().getBaseName()\n + \"-report.txt\");\n }\n } \n}\n```\n\n通过代码,可以发现ProcessApplicationManifest是交给ManifestHelperKt.mergeManifestsForApplication方法对所有Manifest进行合并处理的,并且Log保存在【manifest-merger-${variantname}-report.txt】文件中。\n```\npackage com.android.build.gradle.internal.tasks.manifest\n\n/** Invoke the Manifest Merger version 2. */\nfun mergeManifestsForApplication(\n mainManifest: File,\n manifestOverlays: List<File>,\n dependencies: List<ManifestProvider>,\n navigationFiles: List<File>,\n featureName: String?,\n packageOverride: String?,\n versionCode: Int,\n versionName: String?,\n minSdkVersion: String?,\n targetSdkVersion: String?,\n maxSdkVersion: Int?,\n outManifestLocation: String,\n outAaptSafeManifestLocation: String?,\n outMetadataFeatureManifestLocation: String?,\n outBundleManifestLocation: String?,\n outInstantAppManifestLocation: String?,\n mergeType: ManifestMerger2.MergeType,\n placeHolders: Map<String, Any>,\n optionalFeatures: Collection<ManifestMerger2.Invoker.Feature>,\n reportFile: File?,\n logger: ILogger\n): MergingReport {\n\n try {\n\n //ManifestMerger2是 manifest-merger库提供的辅助类\n val manifestMergerInvoker = ManifestMerger2.newMerger(mainManifest, logger, mergeType)\n .setPlaceHolderValues(placeHolders)\n .addFlavorAndBuildTypeManifests(*manifestOverlays.toTypedArray())\n .addManifestProviders(dependencies)\n .addNavigationFiles(navigationFiles)\n .withFeatures(*optionalFeatures.toTypedArray())\n .setMergeReportFile(reportFile)\n .setFeatureName(featureName)\n\n if (mergeType == ManifestMerger2.MergeType.APPLICATION) {\n manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)\n }\n\n\n if (outAaptSafeManifestLocation != null) {\n manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.MAKE_AAPT_SAFE)\n }\n\n setInjectableValues(\n manifestMergerInvoker,\n packageOverride, versionCode, versionName,\n minSdkVersion, targetSdkVersion, maxSdkVersion\n )\n \n //关注这里的调用\n val mergingReport = manifestMergerInvoker.merge()\n //省略其他对merge结果处理代码\n ... ...\n return mergingReport\n } catch (e: ManifestMerger2.MergeFailureException) {\n // TODO: unacceptable.\n throw RuntimeException(e)\n }\n}\n```\n\n接着看manifestMergerInvoker.merge()的实现\n```\npackage com.android.manifmerger;\n\n/**\n * merges android manifest files, idempotent.\n */\n@Immutable\npublic class ManifestMerger2 {\n public static class Invoker<T extends Invoker<T>>{\n\n @NonNull\n public MergingReport merge() throws MergeFailureException {\n\n // provide some free placeholders values.\n ImmutableMap<ManifestSystemProperty, Object> systemProperties = mSystemProperties.build();\n ... ...\n FileStreamProvider fileStreamProvider = mFileStreamProvider != null\n ? mFileStreamProvider : new FileStreamProvider();\n ManifestMerger2 manifestMerger =\n new ManifestMerger2(\n mLogger,\n mMainManifestFile,\n mLibraryFilesBuilder.build(),\n mFlavorsAndBuildTypeFiles.build(),\n mFeaturesBuilder.build(),\n mPlaceholders.build(),\n new MapBasedKeyBasedValueResolver<ManifestSystemProperty>(\n systemProperties),\n mMergeType,\n mDocumentType,\n Optional.fromNullable(mReportFile),\n mFeatureName,\n fileStreamProvider,\n mNavigationFilesBuilder.build());\n //调用下面的 private MergingReport merge()方法 \n return manifestMerger.merge();\n }\n }\n\n\n /**\n * Perform high level ordering of files merging and delegates actual merging to\n * {@link XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)}\n *\n * @return the merging activity report.\n * @throws MergeFailureException if the merging cannot be completed (for instance, if xml\n * files cannot be loaded).\n */\n @NonNull\n private MergingReport merge() throws MergeFailureException {\n // initiate a new merging report\n MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger);\n //一系列merge manifest规则处理\n ... ...\n MergingReport mergingReport = mergingReportBuilder.build();\n\n if (mReportFile.isPresent()) {\n writeReport(mergingReport);\n }\n return mergingReport;\n }\n\n //最终写入Log文件方法\n /**\n * Creates the merging report file.\n * @param mergingReport the merging activities report to serialize.\n */\n private void writeReport(@NonNull MergingReport mergingReport) {\n FileWriter fileWriter = null;\n ... ... \n fileWriter = new FileWriter(mReportFile.get());\n mergingReport.getActions().log(fileWriter);\n } \n} \n```\n\n到目前为止,从代码层面看到了Log文件是如何生成的。\n# 方案实现\n\n【manifest-merger-${variantname}-report.txt】文件大致内容如下:\n```\n-- Merging decision tree log ---\nmanifest\nADDED from /somepath/AndroidManifest.xml:x:x-xx:xx\nMERGED from [dependencies sdk] /somepath/AndroidManifest.xml:x:x-xx:xx\nINJECTED from /somepath/AndroidManifest.xml:x:x-xx:xx\n...\nuses-permission#android.permission.INTERNET\n```\n\n方案代码实现很简单:\n>1.自定义一个Extension,列出暂禁用的权限;\n>2.实现相应Plugin和Task;\n\nExtension定义可以如下所示:\n```\nhost{\n //明确暂禁用的权限列表\n forbiddenPermissions = ['android.permission.GET_ACCOUNTS',\n 'android.permission.SEND_SMS',\n 'android.permission.CALL_PHONE',\n 'android.permission.BLUETOOTH',\n ... ...] \n}\n```\n\nPlugin简单示例:\n```\npublic class HostPlugin implements Plugin<Project> {\n @Override\n final void apply(Project project) {\n if (!project.getPlugins().hasPlugin('com.android.application') && !project.getPlugins().hasPlugin('com.android.library')) {\n throw new GradleException('apply plugin: \\'com.android.application\\' or apply plugin: \\'com.android.library\\' is required')\n }\n HostExtension hostExtension = project.getExtensions().create('host', HostExtension.class)\n \n project.afterEvaluate {\n def variants = null;\n if (project.plugins.hasPlugin('com.android.application')) {\n variants = android.getApplicationVariants()\n } else if (project.plugins.hasPlugin('com.android.library')) {\n variants = android.getLibraryVariants()\n }\n variants?.all { BaseVariant variant ->\n MergeHostManifestTask taskConfiguration= new MergeHostManifestTask.CreationAction()\n project.getTasks().create(taskConfiguration.getName(), taskConfiguration.getType(), taskConfiguration)\n }\n }\n } \n}\n```\n\nTask简单示例:\n```\nimport org.gradle.util.GFileUtils\nimport com.android.utils.FileUtils\n\nclass MergeHostManifestTask extends DefaultTask {\n\n List<String> forbiddenPermissions //禁用的权限列表\n\n VariantScope scope\n\n @TaskAction\n def doFullTaskAction() {\n\n File logFile = FileUtils.join(\n scope.getGlobalScope().getOutputsDir(),\n \"logs\",\n \"manifest-permissions-validate-\"\n + scope.getVariantConfiguration().getBaseName()\n + \"-report.txt\")\n GFileUtils.mkdirs(logFile.getParentFile())\n GFileUtils.deleteQuietly(logFile) \n\n checkHostManifest(forbiddenPermissions,logFile,scope)\n if (logFile.exists() && logFile.length() > 0) {\n throw new GradleException(\"Has forbidden permissions in host, please check it in file ${logFile.getAbsolutePath()}\")\n } \n } \n\n /**\n * 检测host manifest 是否含有禁用权限列表\n * @param forbiddenPermissions\n * @param logFile\n * @param variantScope\n */\n public static void checkHostManifest(List<String> forbiddenPermissions, File logFile, def variantScope) {\n if (forbiddenPermissions == null || forbiddenPermissions.isEmpty()) {\n return\n }\n\n File reportFile =\n FileUtils.join(\n variantScope.getGlobalScope().getOutputsDir(),\n \"logs\",\n \"manifest-merger-\"\n + variantScope.getVariantConfiguration().getBaseName()\n + \"-report.txt\")\n\n if (!reportFile.exists()) {\n return\n }\n\n reportFile.withReader { reader ->\n String line\n while ((line = reader.readLine()) != null) {\n forbiddenPermissions.each { p ->\n if (line.contains(\"uses-permission#${p.trim()}\")) {\n logFile.append(\"${p.trim()}\\n\")\n logFile.append(reader.readLine())\n logFile.append(\"\\n\")\n }\n }\n }\n }\n }\n\n public static class CreationAction\n extends TaskConfiguration<MergeHostManifestTask> {\n\n BaseVariant variant\n\n Project project\n\n public CreationAction(Project project,BaseVariant variant){\n this.project= project\n this.variant=variant\n }\n\n @Override\n void execute(MergeHostManifestTask task) {\n ... ...\n HostExtension hostExtension = project.getExtensions().findByType(HostExtension.class)\n task.forbiddenPermissions = hostExtension.getForbiddenPermissions()\n task.scope= variant.getMetaClass().getProperty(variant, 'variantData').getScope()\n task.dependsOn getProcessManifestTask()\n }\n\n private Task getProcessManifestTaskCompat() {\n try {\n //>=3.3.0\n String taskName = variant.getMetaClass().getProperty(variant, 'variantData').getScope().getTaskContainer().getProcessManifestTask().getName()\n return project.getTasks().findByName(taskName)\n } catch (Exception e) {\n\n }\n }\n} \n```\n\n如果APP或其依赖的SDK,有引入禁用权限,则会抛出编译异常,生成的【manifest-permissions-validate-${variantname}-report.txt】文件内容类似以下所示:\n```\nandroid.permission.SEND_SMS\nADDED from /../app/src/main/AndroidManifest.xml:9:5-67\nandroid.permission.BLUETOOTH\nADDED from /../app/src/main/AndroidManifest.xml:11:5-68\n```\n\n# 结束语\n关于隐私权限列表,相关部门也未给允一个完整的列表,建议团队把所有未在隐私文档中描述的动态权限都作为禁用权限,直至隐私文档同步。\n\n# 参考\n1.Android Gradle Plugin:[https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core) \n","slug":"一种有效管控APP隐私权限的解决方案","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n870008catea0848knm","content":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>诸如读写外置存储、读取联系人、发短信等隐私权限,android在6.0系统开始进行<a href=\"https://developer.android.com/guide/topics/permissions/overview\">动态授权</a>。但在我国,仅向用户提示授权框还不够,工信部在19年11月初发布了<a href=\"http://www.xinhuanet.com/2019-11/05/c_1125192397.htm\">专项整治App八类侵权行为审明</a> ,其文明确治理以下八类问题:</p>\n<blockquote>\n<p>1.私自收集个人信息;<br>2.超范围收集个人信息;<br>3.私自共享给第三方用户信息;<br>4.强制用户使用定向推送功能;<br>5.不给权限不让用;<br>6.频繁申请权限;<br>7.过度索取权限;<br>8.为用户账号注销设置障碍。 </p>\n</blockquote>\n<p>很不幸,网报通告批评:我司老版本APP中审明了隐私权限,但在隐私文档中并未进行有效说明。收到通告,团队立马对权限进行了扫描,发现APP在AndroidManifest中审明了三项隐私权限,但实际过程并未使用(有些冤大头)。我相信很多团队跟我们面临同个问题,多团队开发下,权限引入问题没有一个有效监管机制。为避免类似问题再次发生,本文给出一个简单有效的代码编译层拦截方案。 </p>\n<p>在说方案原理之前,我们先假定检测方案是扫描APP AndroidManifest.xml文件中审明的和用户有关的隐私权限,再比对隐私文档以及实际使用场景,进行判别。面对检测方案,我们给出解决思路:</p>\n<blockquote>\n<p>在编译阶段processApplicationManifest task运行后,对Merged Manifest Log文件进行扫描,如果用到了新权限,抛出打包错误,直至问题解决;</p>\n</blockquote>\n<span id=\"more\"></span>\n<h1 id=\"源码简阅\"><a href=\"#源码简阅\" class=\"headerlink\" title=\"源码简阅\"></a>源码简阅</h1><p>Android Gradle Plugin在编译APP后,会在build/outputs/logs目录下生成名为【manifest-merger-${variantname}-report.txt】文本文件。<br>以AGP 3.5.0源码为例,简单分析下ProcessApplicationManifest任务是如何产生Merged Manifest Log文件的。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br><span class=\"line\">65</span><br><span class=\"line\">66</span><br><span class=\"line\">67</span><br><span class=\"line\">68</span><br><span class=\"line\">69</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">package com.android.build.gradle.tasks;</span><br><span class=\"line\"></span><br><span class=\"line\">/** A task that processes the manifest */</span><br><span class=\"line\">@CacheableTask</span><br><span class=\"line\">public abstract class ProcessApplicationManifest extends ManifestProcessorTask {</span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> @Internal</span><br><span class=\"line\"> protected boolean getIncremental() {</span><br><span class=\"line\"> return true;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> protected void doFullTaskAction() throws IOException {</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> MergingReport mergingReport =</span><br><span class=\"line\"> ManifestHelperKt.mergeManifestsForApplication(</span><br><span class=\"line\"> getMainManifest(),</span><br><span class=\"line\"> getManifestOverlays(),</span><br><span class=\"line\"> computeFullProviderList(compatibleScreenManifestForSplit),</span><br><span class=\"line\"> navigationXmls,</span><br><span class=\"line\"> getFeatureName(),</span><br><span class=\"line\"> moduleMetadata == null</span><br><span class=\"line\"> ? getPackageOverride()</span><br><span class=\"line\"> : moduleMetadata.getApplicationId(),</span><br><span class=\"line\"> moduleMetadata == null</span><br><span class=\"line\"> ? apkData.getVersionCode()</span><br><span class=\"line\"> : Integer.parseInt(moduleMetadata.getVersionCode()),</span><br><span class=\"line\"> moduleMetadata == null</span><br><span class=\"line\"> ? apkData.getVersionName()</span><br><span class=\"line\"> : moduleMetadata.getVersionName(),</span><br><span class=\"line\"> getMinSdkVersion(),</span><br><span class=\"line\"> getTargetSdkVersion(),</span><br><span class=\"line\"> getMaxSdkVersion(),</span><br><span class=\"line\"> manifestOutputFile.getAbsolutePath(),</span><br><span class=\"line\"> // no aapt friendly merged manifest file necessary for applications.</span><br><span class=\"line\"> null /* aaptFriendlyManifestOutputFile */,</span><br><span class=\"line\"> metadataFeatureManifestOutputFile.getAbsolutePath(),</span><br><span class=\"line\"> bundleManifestOutputFile.getAbsolutePath(),</span><br><span class=\"line\"> instantAppManifestOutputFile != null</span><br><span class=\"line\"> ? instantAppManifestOutputFile.getAbsolutePath()</span><br><span class=\"line\"> : null,</span><br><span class=\"line\"> ManifestMerger2.MergeType.APPLICATION,</span><br><span class=\"line\"> variantConfiguration.getManifestPlaceholders(),</span><br><span class=\"line\"> getOptionalFeatures(),</span><br><span class=\"line\"> getReportFile(), </span><br><span class=\"line\"> LoggerWrapper.getLogger(ProcessApplicationManifest.class));</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> public static class CreationAction</span><br><span class=\"line\"> extends AnnotationProcessingTaskCreationAction<ProcessApplicationManifest> {</span><br><span class=\"line\"></span><br><span class=\"line\"> private File reportFile;</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> public void preConfigure(@NonNull String taskName) {</span><br><span class=\"line\"> super.preConfigure(taskName);</span><br><span class=\"line\"> </span><br><span class=\"line\"> //这里就【manifest-merger-${variantname}-report.txt】文件</span><br><span class=\"line\"> reportFile =</span><br><span class=\"line\"> FileUtils.join(</span><br><span class=\"line\"> variantScope.getGlobalScope().getOutputsDir(),</span><br><span class=\"line\"> "logs",</span><br><span class=\"line\"> "manifest-merger-"</span><br><span class=\"line\"> + variantScope.getVariantConfiguration().getBaseName()</span><br><span class=\"line\"> + "-report.txt");</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>通过代码,可以发现ProcessApplicationManifest是交给ManifestHelperKt.mergeManifestsForApplication方法对所有Manifest进行合并处理的,并且Log保存在【manifest-merger-${variantname}-report.txt】文件中。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">package com.android.build.gradle.internal.tasks.manifest</span><br><span class=\"line\"></span><br><span class=\"line\">/** Invoke the Manifest Merger version 2. */</span><br><span class=\"line\">fun mergeManifestsForApplication(</span><br><span class=\"line\"> mainManifest: File,</span><br><span class=\"line\"> manifestOverlays: List<File>,</span><br><span class=\"line\"> dependencies: List<ManifestProvider>,</span><br><span class=\"line\"> navigationFiles: List<File>,</span><br><span class=\"line\"> featureName: String?,</span><br><span class=\"line\"> packageOverride: String?,</span><br><span class=\"line\"> versionCode: Int,</span><br><span class=\"line\"> versionName: String?,</span><br><span class=\"line\"> minSdkVersion: String?,</span><br><span class=\"line\"> targetSdkVersion: String?,</span><br><span class=\"line\"> maxSdkVersion: Int?,</span><br><span class=\"line\"> outManifestLocation: String,</span><br><span class=\"line\"> outAaptSafeManifestLocation: String?,</span><br><span class=\"line\"> outMetadataFeatureManifestLocation: String?,</span><br><span class=\"line\"> outBundleManifestLocation: String?,</span><br><span class=\"line\"> outInstantAppManifestLocation: String?,</span><br><span class=\"line\"> mergeType: ManifestMerger2.MergeType,</span><br><span class=\"line\"> placeHolders: Map<String, Any>,</span><br><span class=\"line\"> optionalFeatures: Collection<ManifestMerger2.Invoker.Feature>,</span><br><span class=\"line\"> reportFile: File?,</span><br><span class=\"line\"> logger: ILogger</span><br><span class=\"line\">): MergingReport {</span><br><span class=\"line\"></span><br><span class=\"line\"> try {</span><br><span class=\"line\"></span><br><span class=\"line\"> //ManifestMerger2是 manifest-merger库提供的辅助类</span><br><span class=\"line\"> val manifestMergerInvoker = ManifestMerger2.newMerger(mainManifest, logger, mergeType)</span><br><span class=\"line\"> .setPlaceHolderValues(placeHolders)</span><br><span class=\"line\"> .addFlavorAndBuildTypeManifests(*manifestOverlays.toTypedArray())</span><br><span class=\"line\"> .addManifestProviders(dependencies)</span><br><span class=\"line\"> .addNavigationFiles(navigationFiles)</span><br><span class=\"line\"> .withFeatures(*optionalFeatures.toTypedArray())</span><br><span class=\"line\"> .setMergeReportFile(reportFile)</span><br><span class=\"line\"> .setFeatureName(featureName)</span><br><span class=\"line\"></span><br><span class=\"line\"> if (mergeType == ManifestMerger2.MergeType.APPLICATION) {</span><br><span class=\"line\"> manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\"> if (outAaptSafeManifestLocation != null) {</span><br><span class=\"line\"> manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.MAKE_AAPT_SAFE)</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> setInjectableValues(</span><br><span class=\"line\"> manifestMergerInvoker,</span><br><span class=\"line\"> packageOverride, versionCode, versionName,</span><br><span class=\"line\"> minSdkVersion, targetSdkVersion, maxSdkVersion</span><br><span class=\"line\"> )</span><br><span class=\"line\"> </span><br><span class=\"line\"> //关注这里的调用</span><br><span class=\"line\"> val mergingReport = manifestMergerInvoker.merge()</span><br><span class=\"line\"> //省略其他对merge结果处理代码</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> return mergingReport</span><br><span class=\"line\"> } catch (e: ManifestMerger2.MergeFailureException) {</span><br><span class=\"line\"> // TODO: unacceptable.</span><br><span class=\"line\"> throw RuntimeException(e)</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>接着看manifestMergerInvoker.merge()的实现</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br><span class=\"line\">65</span><br><span class=\"line\">66</span><br><span class=\"line\">67</span><br><span class=\"line\">68</span><br><span class=\"line\">69</span><br><span class=\"line\">70</span><br><span class=\"line\">71</span><br><span class=\"line\">72</span><br><span class=\"line\">73</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">package com.android.manifmerger;</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * merges android manifest files, idempotent.</span><br><span class=\"line\"> */</span><br><span class=\"line\">@Immutable</span><br><span class=\"line\">public class ManifestMerger2 {</span><br><span class=\"line\"> public static class Invoker<T extends Invoker<T>>{</span><br><span class=\"line\"></span><br><span class=\"line\"> @NonNull</span><br><span class=\"line\"> public MergingReport merge() throws MergeFailureException {</span><br><span class=\"line\"></span><br><span class=\"line\"> // provide some free placeholders values.</span><br><span class=\"line\"> ImmutableMap<ManifestSystemProperty, Object> systemProperties = mSystemProperties.build();</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> FileStreamProvider fileStreamProvider = mFileStreamProvider != null</span><br><span class=\"line\"> ? mFileStreamProvider : new FileStreamProvider();</span><br><span class=\"line\"> ManifestMerger2 manifestMerger =</span><br><span class=\"line\"> new ManifestMerger2(</span><br><span class=\"line\"> mLogger,</span><br><span class=\"line\"> mMainManifestFile,</span><br><span class=\"line\"> mLibraryFilesBuilder.build(),</span><br><span class=\"line\"> mFlavorsAndBuildTypeFiles.build(),</span><br><span class=\"line\"> mFeaturesBuilder.build(),</span><br><span class=\"line\"> mPlaceholders.build(),</span><br><span class=\"line\"> new MapBasedKeyBasedValueResolver<ManifestSystemProperty>(</span><br><span class=\"line\"> systemProperties),</span><br><span class=\"line\"> mMergeType,</span><br><span class=\"line\"> mDocumentType,</span><br><span class=\"line\"> Optional.fromNullable(mReportFile),</span><br><span class=\"line\"> mFeatureName,</span><br><span class=\"line\"> fileStreamProvider,</span><br><span class=\"line\"> mNavigationFilesBuilder.build());</span><br><span class=\"line\"> //调用下面的 private MergingReport merge()方法 </span><br><span class=\"line\"> return manifestMerger.merge();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * Perform high level ordering of files merging and delegates actual merging to</span><br><span class=\"line\"> * {@link XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)}</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @return the merging activity report.</span><br><span class=\"line\"> * @throws MergeFailureException if the merging cannot be completed (for instance, if xml</span><br><span class=\"line\"> * files cannot be loaded).</span><br><span class=\"line\"> */</span><br><span class=\"line\"> @NonNull</span><br><span class=\"line\"> private MergingReport merge() throws MergeFailureException {</span><br><span class=\"line\"> // initiate a new merging report</span><br><span class=\"line\"> MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger);</span><br><span class=\"line\"> //一系列merge manifest规则处理</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> MergingReport mergingReport = mergingReportBuilder.build();</span><br><span class=\"line\"></span><br><span class=\"line\"> if (mReportFile.isPresent()) {</span><br><span class=\"line\"> writeReport(mergingReport);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return mergingReport;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> //最终写入Log文件方法</span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * Creates the merging report file.</span><br><span class=\"line\"> * @param mergingReport the merging activities report to serialize.</span><br><span class=\"line\"> */</span><br><span class=\"line\"> private void writeReport(@NonNull MergingReport mergingReport) {</span><br><span class=\"line\"> FileWriter fileWriter = null;</span><br><span class=\"line\"> ... ... </span><br><span class=\"line\"> fileWriter = new FileWriter(mReportFile.get());</span><br><span class=\"line\"> mergingReport.getActions().log(fileWriter);</span><br><span class=\"line\"> } </span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<p>到目前为止,从代码层面看到了Log文件是如何生成的。</p>\n<h1 id=\"方案实现\"><a href=\"#方案实现\" class=\"headerlink\" title=\"方案实现\"></a>方案实现</h1><p>【manifest-merger-${variantname}-report.txt】文件大致内容如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">-- Merging decision tree log ---</span><br><span class=\"line\">manifest</span><br><span class=\"line\">ADDED from /somepath/AndroidManifest.xml:x:x-xx:xx</span><br><span class=\"line\">MERGED from [dependencies sdk] /somepath/AndroidManifest.xml:x:x-xx:xx</span><br><span class=\"line\">INJECTED from /somepath/AndroidManifest.xml:x:x-xx:xx</span><br><span class=\"line\">...</span><br><span class=\"line\">uses-permission#android.permission.INTERNET</span><br></pre></td></tr></table></figure>\n\n<p>方案代码实现很简单:</p>\n<blockquote>\n<p>1.自定义一个Extension,列出暂禁用的权限;<br>2.实现相应Plugin和Task;</p>\n</blockquote>\n<p>Extension定义可以如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">host{</span><br><span class=\"line\"> //明确暂禁用的权限列表</span><br><span class=\"line\"> forbiddenPermissions = ['android.permission.GET_ACCOUNTS',</span><br><span class=\"line\"> 'android.permission.SEND_SMS',</span><br><span class=\"line\"> 'android.permission.CALL_PHONE',</span><br><span class=\"line\"> 'android.permission.BLUETOOTH',</span><br><span class=\"line\"> ... ...] </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>Plugin简单示例:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">public class HostPlugin implements Plugin<Project> {</span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> final void apply(Project project) {</span><br><span class=\"line\"> if (!project.getPlugins().hasPlugin('com.android.application') && !project.getPlugins().hasPlugin('com.android.library')) {</span><br><span class=\"line\"> throw new GradleException('apply plugin: \\'com.android.application\\' or apply plugin: \\'com.android.library\\' is required')</span><br><span class=\"line\"> }</span><br><span class=\"line\"> HostExtension hostExtension = project.getExtensions().create('host', HostExtension.class)</span><br><span class=\"line\"> </span><br><span class=\"line\"> project.afterEvaluate {</span><br><span class=\"line\"> def variants = null;</span><br><span class=\"line\"> if (project.plugins.hasPlugin('com.android.application')) {</span><br><span class=\"line\"> variants = android.getApplicationVariants()</span><br><span class=\"line\"> } else if (project.plugins.hasPlugin('com.android.library')) {</span><br><span class=\"line\"> variants = android.getLibraryVariants()</span><br><span class=\"line\"> }</span><br><span class=\"line\"> variants?.all { BaseVariant variant -></span><br><span class=\"line\"> MergeHostManifestTask taskConfiguration= new MergeHostManifestTask.CreationAction()</span><br><span class=\"line\"> project.getTasks().create(taskConfiguration.getName(), taskConfiguration.getType(), taskConfiguration)</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>Task简单示例:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br><span class=\"line\">65</span><br><span class=\"line\">66</span><br><span class=\"line\">67</span><br><span class=\"line\">68</span><br><span class=\"line\">69</span><br><span class=\"line\">70</span><br><span class=\"line\">71</span><br><span class=\"line\">72</span><br><span class=\"line\">73</span><br><span class=\"line\">74</span><br><span class=\"line\">75</span><br><span class=\"line\">76</span><br><span class=\"line\">77</span><br><span class=\"line\">78</span><br><span class=\"line\">79</span><br><span class=\"line\">80</span><br><span class=\"line\">81</span><br><span class=\"line\">82</span><br><span class=\"line\">83</span><br><span class=\"line\">84</span><br><span class=\"line\">85</span><br><span class=\"line\">86</span><br><span class=\"line\">87</span><br><span class=\"line\">88</span><br><span class=\"line\">89</span><br><span class=\"line\">90</span><br><span class=\"line\">91</span><br><span class=\"line\">92</span><br><span class=\"line\">93</span><br><span class=\"line\">94</span><br><span class=\"line\">95</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">import org.gradle.util.GFileUtils</span><br><span class=\"line\">import com.android.utils.FileUtils</span><br><span class=\"line\"></span><br><span class=\"line\">class MergeHostManifestTask extends DefaultTask {</span><br><span class=\"line\"></span><br><span class=\"line\"> List<String> forbiddenPermissions //禁用的权限列表</span><br><span class=\"line\"></span><br><span class=\"line\"> VariantScope scope</span><br><span class=\"line\"></span><br><span class=\"line\"> @TaskAction</span><br><span class=\"line\"> def doFullTaskAction() {</span><br><span class=\"line\"></span><br><span class=\"line\"> File logFile = FileUtils.join(</span><br><span class=\"line\"> scope.getGlobalScope().getOutputsDir(),</span><br><span class=\"line\"> "logs",</span><br><span class=\"line\"> "manifest-permissions-validate-"</span><br><span class=\"line\"> + scope.getVariantConfiguration().getBaseName()</span><br><span class=\"line\"> + "-report.txt")</span><br><span class=\"line\"> GFileUtils.mkdirs(logFile.getParentFile())</span><br><span class=\"line\"> GFileUtils.deleteQuietly(logFile) </span><br><span class=\"line\"></span><br><span class=\"line\"> checkHostManifest(forbiddenPermissions,logFile,scope)</span><br><span class=\"line\"> if (logFile.exists() && logFile.length() > 0) {</span><br><span class=\"line\"> throw new GradleException("Has forbidden permissions in host, please check it in file ${logFile.getAbsolutePath()}")</span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\"></span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * 检测host manifest 是否含有禁用权限列表</span><br><span class=\"line\"> * @param forbiddenPermissions</span><br><span class=\"line\"> * @param logFile</span><br><span class=\"line\"> * @param variantScope</span><br><span class=\"line\"> */</span><br><span class=\"line\"> public static void checkHostManifest(List<String> forbiddenPermissions, File logFile, def variantScope) {</span><br><span class=\"line\"> if (forbiddenPermissions == null || forbiddenPermissions.isEmpty()) {</span><br><span class=\"line\"> return</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> File reportFile =</span><br><span class=\"line\"> FileUtils.join(</span><br><span class=\"line\"> variantScope.getGlobalScope().getOutputsDir(),</span><br><span class=\"line\"> "logs",</span><br><span class=\"line\"> "manifest-merger-"</span><br><span class=\"line\"> + variantScope.getVariantConfiguration().getBaseName()</span><br><span class=\"line\"> + "-report.txt")</span><br><span class=\"line\"></span><br><span class=\"line\"> if (!reportFile.exists()) {</span><br><span class=\"line\"> return</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> reportFile.withReader { reader -></span><br><span class=\"line\"> String line</span><br><span class=\"line\"> while ((line = reader.readLine()) != null) {</span><br><span class=\"line\"> forbiddenPermissions.each { p -></span><br><span class=\"line\"> if (line.contains("uses-permission#${p.trim()}")) {</span><br><span class=\"line\"> logFile.append("${p.trim()}\\n")</span><br><span class=\"line\"> logFile.append(reader.readLine())</span><br><span class=\"line\"> logFile.append("\\n")</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> public static class CreationAction</span><br><span class=\"line\"> extends TaskConfiguration<MergeHostManifestTask> {</span><br><span class=\"line\"></span><br><span class=\"line\"> BaseVariant variant</span><br><span class=\"line\"></span><br><span class=\"line\"> Project project</span><br><span class=\"line\"></span><br><span class=\"line\"> public CreationAction(Project project,BaseVariant variant){</span><br><span class=\"line\"> this.project= project</span><br><span class=\"line\"> this.variant=variant</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> void execute(MergeHostManifestTask task) {</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> HostExtension hostExtension = project.getExtensions().findByType(HostExtension.class)</span><br><span class=\"line\"> task.forbiddenPermissions = hostExtension.getForbiddenPermissions()</span><br><span class=\"line\"> task.scope= variant.getMetaClass().getProperty(variant, 'variantData').getScope()</span><br><span class=\"line\"> task.dependsOn getProcessManifestTask()</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> private Task getProcessManifestTaskCompat() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> //>=3.3.0</span><br><span class=\"line\"> String taskName = variant.getMetaClass().getProperty(variant, 'variantData').getScope().getTaskContainer().getProcessManifestTask().getName()</span><br><span class=\"line\"> return project.getTasks().findByName(taskName)</span><br><span class=\"line\"> } catch (Exception e) {</span><br><span class=\"line\"></span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<p>如果APP或其依赖的SDK,有引入禁用权限,则会抛出编译异常,生成的【manifest-permissions-validate-${variantname}-report.txt】文件内容类似以下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">android.permission.SEND_SMS</span><br><span class=\"line\">ADDED from /../app/src/main/AndroidManifest.xml:9:5-67</span><br><span class=\"line\">android.permission.BLUETOOTH</span><br><span class=\"line\">ADDED from /../app/src/main/AndroidManifest.xml:11:5-68</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>关于隐私权限列表,相关部门也未给允一个完整的列表,建议团队把所有未在隐私文档中描述的动态权限都作为禁用权限,直至隐私文档同步。</p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>1.Android Gradle Plugin:<a href=\"https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core\">https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core</a> </p>\n","excerpt":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>诸如读写外置存储、读取联系人、发短信等隐私权限,android在6.0系统开始进行<a href=\"https://developer.android.com/guide/topics/permissions/overview\">动态授权</a>。但在我国,仅向用户提示授权框还不够,工信部在19年11月初发布了<a href=\"http://www.xinhuanet.com/2019-11/05/c_1125192397.htm\">专项整治App八类侵权行为审明</a> ,其文明确治理以下八类问题:</p>\n<blockquote>\n<p>1.私自收集个人信息;<br>2.超范围收集个人信息;<br>3.私自共享给第三方用户信息;<br>4.强制用户使用定向推送功能;<br>5.不给权限不让用;<br>6.频繁申请权限;<br>7.过度索取权限;<br>8.为用户账号注销设置障碍。 </p>\n</blockquote>\n<p>很不幸,网报通告批评:我司老版本APP中审明了隐私权限,但在隐私文档中并未进行有效说明。收到通告,团队立马对权限进行了扫描,发现APP在AndroidManifest中审明了三项隐私权限,但实际过程并未使用(有些冤大头)。我相信很多团队跟我们面临同个问题,多团队开发下,权限引入问题没有一个有效监管机制。为避免类似问题再次发生,本文给出一个简单有效的代码编译层拦截方案。 </p>\n<p>在说方案原理之前,我们先假定检测方案是扫描APP AndroidManifest.xml文件中审明的和用户有关的隐私权限,再比对隐私文档以及实际使用场景,进行判别。面对检测方案,我们给出解决思路:</p>\n<blockquote>\n<p>在编译阶段processApplicationManifest task运行后,对Merged Manifest Log文件进行扫描,如果用到了新权限,抛出打包错误,直至问题解决;</p>\n</blockquote>","more":"<h1 id=\"源码简阅\"><a href=\"#源码简阅\" class=\"headerlink\" title=\"源码简阅\"></a>源码简阅</h1><p>Android Gradle Plugin在编译APP后,会在build/outputs/logs目录下生成名为【manifest-merger-${variantname}-report.txt】文本文件。<br>以AGP 3.5.0源码为例,简单分析下ProcessApplicationManifest任务是如何产生Merged Manifest Log文件的。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br><span class=\"line\">65</span><br><span class=\"line\">66</span><br><span class=\"line\">67</span><br><span class=\"line\">68</span><br><span class=\"line\">69</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">package com.android.build.gradle.tasks;</span><br><span class=\"line\"></span><br><span class=\"line\">/** A task that processes the manifest */</span><br><span class=\"line\">@CacheableTask</span><br><span class=\"line\">public abstract class ProcessApplicationManifest extends ManifestProcessorTask {</span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> @Internal</span><br><span class=\"line\"> protected boolean getIncremental() {</span><br><span class=\"line\"> return true;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> protected void doFullTaskAction() throws IOException {</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> MergingReport mergingReport =</span><br><span class=\"line\"> ManifestHelperKt.mergeManifestsForApplication(</span><br><span class=\"line\"> getMainManifest(),</span><br><span class=\"line\"> getManifestOverlays(),</span><br><span class=\"line\"> computeFullProviderList(compatibleScreenManifestForSplit),</span><br><span class=\"line\"> navigationXmls,</span><br><span class=\"line\"> getFeatureName(),</span><br><span class=\"line\"> moduleMetadata == null</span><br><span class=\"line\"> ? getPackageOverride()</span><br><span class=\"line\"> : moduleMetadata.getApplicationId(),</span><br><span class=\"line\"> moduleMetadata == null</span><br><span class=\"line\"> ? apkData.getVersionCode()</span><br><span class=\"line\"> : Integer.parseInt(moduleMetadata.getVersionCode()),</span><br><span class=\"line\"> moduleMetadata == null</span><br><span class=\"line\"> ? apkData.getVersionName()</span><br><span class=\"line\"> : moduleMetadata.getVersionName(),</span><br><span class=\"line\"> getMinSdkVersion(),</span><br><span class=\"line\"> getTargetSdkVersion(),</span><br><span class=\"line\"> getMaxSdkVersion(),</span><br><span class=\"line\"> manifestOutputFile.getAbsolutePath(),</span><br><span class=\"line\"> // no aapt friendly merged manifest file necessary for applications.</span><br><span class=\"line\"> null /* aaptFriendlyManifestOutputFile */,</span><br><span class=\"line\"> metadataFeatureManifestOutputFile.getAbsolutePath(),</span><br><span class=\"line\"> bundleManifestOutputFile.getAbsolutePath(),</span><br><span class=\"line\"> instantAppManifestOutputFile != null</span><br><span class=\"line\"> ? instantAppManifestOutputFile.getAbsolutePath()</span><br><span class=\"line\"> : null,</span><br><span class=\"line\"> ManifestMerger2.MergeType.APPLICATION,</span><br><span class=\"line\"> variantConfiguration.getManifestPlaceholders(),</span><br><span class=\"line\"> getOptionalFeatures(),</span><br><span class=\"line\"> getReportFile(), </span><br><span class=\"line\"> LoggerWrapper.getLogger(ProcessApplicationManifest.class));</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> public static class CreationAction</span><br><span class=\"line\"> extends AnnotationProcessingTaskCreationAction<ProcessApplicationManifest> {</span><br><span class=\"line\"></span><br><span class=\"line\"> private File reportFile;</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> public void preConfigure(@NonNull String taskName) {</span><br><span class=\"line\"> super.preConfigure(taskName);</span><br><span class=\"line\"> </span><br><span class=\"line\"> //这里就【manifest-merger-${variantname}-report.txt】文件</span><br><span class=\"line\"> reportFile =</span><br><span class=\"line\"> FileUtils.join(</span><br><span class=\"line\"> variantScope.getGlobalScope().getOutputsDir(),</span><br><span class=\"line\"> "logs",</span><br><span class=\"line\"> "manifest-merger-"</span><br><span class=\"line\"> + variantScope.getVariantConfiguration().getBaseName()</span><br><span class=\"line\"> + "-report.txt");</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>通过代码,可以发现ProcessApplicationManifest是交给ManifestHelperKt.mergeManifestsForApplication方法对所有Manifest进行合并处理的,并且Log保存在【manifest-merger-${variantname}-report.txt】文件中。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">package com.android.build.gradle.internal.tasks.manifest</span><br><span class=\"line\"></span><br><span class=\"line\">/** Invoke the Manifest Merger version 2. */</span><br><span class=\"line\">fun mergeManifestsForApplication(</span><br><span class=\"line\"> mainManifest: File,</span><br><span class=\"line\"> manifestOverlays: List<File>,</span><br><span class=\"line\"> dependencies: List<ManifestProvider>,</span><br><span class=\"line\"> navigationFiles: List<File>,</span><br><span class=\"line\"> featureName: String?,</span><br><span class=\"line\"> packageOverride: String?,</span><br><span class=\"line\"> versionCode: Int,</span><br><span class=\"line\"> versionName: String?,</span><br><span class=\"line\"> minSdkVersion: String?,</span><br><span class=\"line\"> targetSdkVersion: String?,</span><br><span class=\"line\"> maxSdkVersion: Int?,</span><br><span class=\"line\"> outManifestLocation: String,</span><br><span class=\"line\"> outAaptSafeManifestLocation: String?,</span><br><span class=\"line\"> outMetadataFeatureManifestLocation: String?,</span><br><span class=\"line\"> outBundleManifestLocation: String?,</span><br><span class=\"line\"> outInstantAppManifestLocation: String?,</span><br><span class=\"line\"> mergeType: ManifestMerger2.MergeType,</span><br><span class=\"line\"> placeHolders: Map<String, Any>,</span><br><span class=\"line\"> optionalFeatures: Collection<ManifestMerger2.Invoker.Feature>,</span><br><span class=\"line\"> reportFile: File?,</span><br><span class=\"line\"> logger: ILogger</span><br><span class=\"line\">): MergingReport {</span><br><span class=\"line\"></span><br><span class=\"line\"> try {</span><br><span class=\"line\"></span><br><span class=\"line\"> //ManifestMerger2是 manifest-merger库提供的辅助类</span><br><span class=\"line\"> val manifestMergerInvoker = ManifestMerger2.newMerger(mainManifest, logger, mergeType)</span><br><span class=\"line\"> .setPlaceHolderValues(placeHolders)</span><br><span class=\"line\"> .addFlavorAndBuildTypeManifests(*manifestOverlays.toTypedArray())</span><br><span class=\"line\"> .addManifestProviders(dependencies)</span><br><span class=\"line\"> .addNavigationFiles(navigationFiles)</span><br><span class=\"line\"> .withFeatures(*optionalFeatures.toTypedArray())</span><br><span class=\"line\"> .setMergeReportFile(reportFile)</span><br><span class=\"line\"> .setFeatureName(featureName)</span><br><span class=\"line\"></span><br><span class=\"line\"> if (mergeType == ManifestMerger2.MergeType.APPLICATION) {</span><br><span class=\"line\"> manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.REMOVE_TOOLS_DECLARATIONS)</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\"> if (outAaptSafeManifestLocation != null) {</span><br><span class=\"line\"> manifestMergerInvoker.withFeatures(ManifestMerger2.Invoker.Feature.MAKE_AAPT_SAFE)</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> setInjectableValues(</span><br><span class=\"line\"> manifestMergerInvoker,</span><br><span class=\"line\"> packageOverride, versionCode, versionName,</span><br><span class=\"line\"> minSdkVersion, targetSdkVersion, maxSdkVersion</span><br><span class=\"line\"> )</span><br><span class=\"line\"> </span><br><span class=\"line\"> //关注这里的调用</span><br><span class=\"line\"> val mergingReport = manifestMergerInvoker.merge()</span><br><span class=\"line\"> //省略其他对merge结果处理代码</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> return mergingReport</span><br><span class=\"line\"> } catch (e: ManifestMerger2.MergeFailureException) {</span><br><span class=\"line\"> // TODO: unacceptable.</span><br><span class=\"line\"> throw RuntimeException(e)</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>接着看manifestMergerInvoker.merge()的实现</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br><span class=\"line\">65</span><br><span class=\"line\">66</span><br><span class=\"line\">67</span><br><span class=\"line\">68</span><br><span class=\"line\">69</span><br><span class=\"line\">70</span><br><span class=\"line\">71</span><br><span class=\"line\">72</span><br><span class=\"line\">73</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">package com.android.manifmerger;</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * merges android manifest files, idempotent.</span><br><span class=\"line\"> */</span><br><span class=\"line\">@Immutable</span><br><span class=\"line\">public class ManifestMerger2 {</span><br><span class=\"line\"> public static class Invoker<T extends Invoker<T>>{</span><br><span class=\"line\"></span><br><span class=\"line\"> @NonNull</span><br><span class=\"line\"> public MergingReport merge() throws MergeFailureException {</span><br><span class=\"line\"></span><br><span class=\"line\"> // provide some free placeholders values.</span><br><span class=\"line\"> ImmutableMap<ManifestSystemProperty, Object> systemProperties = mSystemProperties.build();</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> FileStreamProvider fileStreamProvider = mFileStreamProvider != null</span><br><span class=\"line\"> ? mFileStreamProvider : new FileStreamProvider();</span><br><span class=\"line\"> ManifestMerger2 manifestMerger =</span><br><span class=\"line\"> new ManifestMerger2(</span><br><span class=\"line\"> mLogger,</span><br><span class=\"line\"> mMainManifestFile,</span><br><span class=\"line\"> mLibraryFilesBuilder.build(),</span><br><span class=\"line\"> mFlavorsAndBuildTypeFiles.build(),</span><br><span class=\"line\"> mFeaturesBuilder.build(),</span><br><span class=\"line\"> mPlaceholders.build(),</span><br><span class=\"line\"> new MapBasedKeyBasedValueResolver<ManifestSystemProperty>(</span><br><span class=\"line\"> systemProperties),</span><br><span class=\"line\"> mMergeType,</span><br><span class=\"line\"> mDocumentType,</span><br><span class=\"line\"> Optional.fromNullable(mReportFile),</span><br><span class=\"line\"> mFeatureName,</span><br><span class=\"line\"> fileStreamProvider,</span><br><span class=\"line\"> mNavigationFilesBuilder.build());</span><br><span class=\"line\"> //调用下面的 private MergingReport merge()方法 </span><br><span class=\"line\"> return manifestMerger.merge();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * Perform high level ordering of files merging and delegates actual merging to</span><br><span class=\"line\"> * {@link XmlDocument#merge(XmlDocument, com.android.manifmerger.MergingReport.Builder)}</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @return the merging activity report.</span><br><span class=\"line\"> * @throws MergeFailureException if the merging cannot be completed (for instance, if xml</span><br><span class=\"line\"> * files cannot be loaded).</span><br><span class=\"line\"> */</span><br><span class=\"line\"> @NonNull</span><br><span class=\"line\"> private MergingReport merge() throws MergeFailureException {</span><br><span class=\"line\"> // initiate a new merging report</span><br><span class=\"line\"> MergingReport.Builder mergingReportBuilder = new MergingReport.Builder(mLogger);</span><br><span class=\"line\"> //一系列merge manifest规则处理</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> MergingReport mergingReport = mergingReportBuilder.build();</span><br><span class=\"line\"></span><br><span class=\"line\"> if (mReportFile.isPresent()) {</span><br><span class=\"line\"> writeReport(mergingReport);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return mergingReport;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> //最终写入Log文件方法</span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * Creates the merging report file.</span><br><span class=\"line\"> * @param mergingReport the merging activities report to serialize.</span><br><span class=\"line\"> */</span><br><span class=\"line\"> private void writeReport(@NonNull MergingReport mergingReport) {</span><br><span class=\"line\"> FileWriter fileWriter = null;</span><br><span class=\"line\"> ... ... </span><br><span class=\"line\"> fileWriter = new FileWriter(mReportFile.get());</span><br><span class=\"line\"> mergingReport.getActions().log(fileWriter);</span><br><span class=\"line\"> } </span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<p>到目前为止,从代码层面看到了Log文件是如何生成的。</p>\n<h1 id=\"方案实现\"><a href=\"#方案实现\" class=\"headerlink\" title=\"方案实现\"></a>方案实现</h1><p>【manifest-merger-${variantname}-report.txt】文件大致内容如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">-- Merging decision tree log ---</span><br><span class=\"line\">manifest</span><br><span class=\"line\">ADDED from /somepath/AndroidManifest.xml:x:x-xx:xx</span><br><span class=\"line\">MERGED from [dependencies sdk] /somepath/AndroidManifest.xml:x:x-xx:xx</span><br><span class=\"line\">INJECTED from /somepath/AndroidManifest.xml:x:x-xx:xx</span><br><span class=\"line\">...</span><br><span class=\"line\">uses-permission#android.permission.INTERNET</span><br></pre></td></tr></table></figure>\n\n<p>方案代码实现很简单:</p>\n<blockquote>\n<p>1.自定义一个Extension,列出暂禁用的权限;<br>2.实现相应Plugin和Task;</p>\n</blockquote>\n<p>Extension定义可以如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">host{</span><br><span class=\"line\"> //明确暂禁用的权限列表</span><br><span class=\"line\"> forbiddenPermissions = ['android.permission.GET_ACCOUNTS',</span><br><span class=\"line\"> 'android.permission.SEND_SMS',</span><br><span class=\"line\"> 'android.permission.CALL_PHONE',</span><br><span class=\"line\"> 'android.permission.BLUETOOTH',</span><br><span class=\"line\"> ... ...] </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>Plugin简单示例:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">public class HostPlugin implements Plugin<Project> {</span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> final void apply(Project project) {</span><br><span class=\"line\"> if (!project.getPlugins().hasPlugin('com.android.application') && !project.getPlugins().hasPlugin('com.android.library')) {</span><br><span class=\"line\"> throw new GradleException('apply plugin: \\'com.android.application\\' or apply plugin: \\'com.android.library\\' is required')</span><br><span class=\"line\"> }</span><br><span class=\"line\"> HostExtension hostExtension = project.getExtensions().create('host', HostExtension.class)</span><br><span class=\"line\"> </span><br><span class=\"line\"> project.afterEvaluate {</span><br><span class=\"line\"> def variants = null;</span><br><span class=\"line\"> if (project.plugins.hasPlugin('com.android.application')) {</span><br><span class=\"line\"> variants = android.getApplicationVariants()</span><br><span class=\"line\"> } else if (project.plugins.hasPlugin('com.android.library')) {</span><br><span class=\"line\"> variants = android.getLibraryVariants()</span><br><span class=\"line\"> }</span><br><span class=\"line\"> variants?.all { BaseVariant variant -></span><br><span class=\"line\"> MergeHostManifestTask taskConfiguration= new MergeHostManifestTask.CreationAction()</span><br><span class=\"line\"> project.getTasks().create(taskConfiguration.getName(), taskConfiguration.getType(), taskConfiguration)</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>Task简单示例:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br><span class=\"line\">51</span><br><span class=\"line\">52</span><br><span class=\"line\">53</span><br><span class=\"line\">54</span><br><span class=\"line\">55</span><br><span class=\"line\">56</span><br><span class=\"line\">57</span><br><span class=\"line\">58</span><br><span class=\"line\">59</span><br><span class=\"line\">60</span><br><span class=\"line\">61</span><br><span class=\"line\">62</span><br><span class=\"line\">63</span><br><span class=\"line\">64</span><br><span class=\"line\">65</span><br><span class=\"line\">66</span><br><span class=\"line\">67</span><br><span class=\"line\">68</span><br><span class=\"line\">69</span><br><span class=\"line\">70</span><br><span class=\"line\">71</span><br><span class=\"line\">72</span><br><span class=\"line\">73</span><br><span class=\"line\">74</span><br><span class=\"line\">75</span><br><span class=\"line\">76</span><br><span class=\"line\">77</span><br><span class=\"line\">78</span><br><span class=\"line\">79</span><br><span class=\"line\">80</span><br><span class=\"line\">81</span><br><span class=\"line\">82</span><br><span class=\"line\">83</span><br><span class=\"line\">84</span><br><span class=\"line\">85</span><br><span class=\"line\">86</span><br><span class=\"line\">87</span><br><span class=\"line\">88</span><br><span class=\"line\">89</span><br><span class=\"line\">90</span><br><span class=\"line\">91</span><br><span class=\"line\">92</span><br><span class=\"line\">93</span><br><span class=\"line\">94</span><br><span class=\"line\">95</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">import org.gradle.util.GFileUtils</span><br><span class=\"line\">import com.android.utils.FileUtils</span><br><span class=\"line\"></span><br><span class=\"line\">class MergeHostManifestTask extends DefaultTask {</span><br><span class=\"line\"></span><br><span class=\"line\"> List<String> forbiddenPermissions //禁用的权限列表</span><br><span class=\"line\"></span><br><span class=\"line\"> VariantScope scope</span><br><span class=\"line\"></span><br><span class=\"line\"> @TaskAction</span><br><span class=\"line\"> def doFullTaskAction() {</span><br><span class=\"line\"></span><br><span class=\"line\"> File logFile = FileUtils.join(</span><br><span class=\"line\"> scope.getGlobalScope().getOutputsDir(),</span><br><span class=\"line\"> "logs",</span><br><span class=\"line\"> "manifest-permissions-validate-"</span><br><span class=\"line\"> + scope.getVariantConfiguration().getBaseName()</span><br><span class=\"line\"> + "-report.txt")</span><br><span class=\"line\"> GFileUtils.mkdirs(logFile.getParentFile())</span><br><span class=\"line\"> GFileUtils.deleteQuietly(logFile) </span><br><span class=\"line\"></span><br><span class=\"line\"> checkHostManifest(forbiddenPermissions,logFile,scope)</span><br><span class=\"line\"> if (logFile.exists() && logFile.length() > 0) {</span><br><span class=\"line\"> throw new GradleException("Has forbidden permissions in host, please check it in file ${logFile.getAbsolutePath()}")</span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\"></span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * 检测host manifest 是否含有禁用权限列表</span><br><span class=\"line\"> * @param forbiddenPermissions</span><br><span class=\"line\"> * @param logFile</span><br><span class=\"line\"> * @param variantScope</span><br><span class=\"line\"> */</span><br><span class=\"line\"> public static void checkHostManifest(List<String> forbiddenPermissions, File logFile, def variantScope) {</span><br><span class=\"line\"> if (forbiddenPermissions == null || forbiddenPermissions.isEmpty()) {</span><br><span class=\"line\"> return</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> File reportFile =</span><br><span class=\"line\"> FileUtils.join(</span><br><span class=\"line\"> variantScope.getGlobalScope().getOutputsDir(),</span><br><span class=\"line\"> "logs",</span><br><span class=\"line\"> "manifest-merger-"</span><br><span class=\"line\"> + variantScope.getVariantConfiguration().getBaseName()</span><br><span class=\"line\"> + "-report.txt")</span><br><span class=\"line\"></span><br><span class=\"line\"> if (!reportFile.exists()) {</span><br><span class=\"line\"> return</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> reportFile.withReader { reader -></span><br><span class=\"line\"> String line</span><br><span class=\"line\"> while ((line = reader.readLine()) != null) {</span><br><span class=\"line\"> forbiddenPermissions.each { p -></span><br><span class=\"line\"> if (line.contains("uses-permission#${p.trim()}")) {</span><br><span class=\"line\"> logFile.append("${p.trim()}\\n")</span><br><span class=\"line\"> logFile.append(reader.readLine())</span><br><span class=\"line\"> logFile.append("\\n")</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> public static class CreationAction</span><br><span class=\"line\"> extends TaskConfiguration<MergeHostManifestTask> {</span><br><span class=\"line\"></span><br><span class=\"line\"> BaseVariant variant</span><br><span class=\"line\"></span><br><span class=\"line\"> Project project</span><br><span class=\"line\"></span><br><span class=\"line\"> public CreationAction(Project project,BaseVariant variant){</span><br><span class=\"line\"> this.project= project</span><br><span class=\"line\"> this.variant=variant</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> void execute(MergeHostManifestTask task) {</span><br><span class=\"line\"> ... ...</span><br><span class=\"line\"> HostExtension hostExtension = project.getExtensions().findByType(HostExtension.class)</span><br><span class=\"line\"> task.forbiddenPermissions = hostExtension.getForbiddenPermissions()</span><br><span class=\"line\"> task.scope= variant.getMetaClass().getProperty(variant, 'variantData').getScope()</span><br><span class=\"line\"> task.dependsOn getProcessManifestTask()</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> private Task getProcessManifestTaskCompat() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> //>=3.3.0</span><br><span class=\"line\"> String taskName = variant.getMetaClass().getProperty(variant, 'variantData').getScope().getTaskContainer().getProcessManifestTask().getName()</span><br><span class=\"line\"> return project.getTasks().findByName(taskName)</span><br><span class=\"line\"> } catch (Exception e) {</span><br><span class=\"line\"></span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<p>如果APP或其依赖的SDK,有引入禁用权限,则会抛出编译异常,生成的【manifest-permissions-validate-${variantname}-report.txt】文件内容类似以下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">android.permission.SEND_SMS</span><br><span class=\"line\">ADDED from /../app/src/main/AndroidManifest.xml:9:5-67</span><br><span class=\"line\">android.permission.BLUETOOTH</span><br><span class=\"line\">ADDED from /../app/src/main/AndroidManifest.xml:11:5-68</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>关于隐私权限列表,相关部门也未给允一个完整的列表,建议团队把所有未在隐私文档中描述的动态权限都作为禁用权限,直至隐私文档同步。</p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>1.Android Gradle Plugin:<a href=\"https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core\">https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core</a> </p>"},{"title":"介绍两款androidX迁移利器","date":"2019-12-12T11:23:57.000Z","_content":"# 背景\nsupport迁移至androidX相关包,绝大部分团队万不得已才处理,因其可见的收益过小,反而带来更大研发和测试成本。 \n考虑androidX相关包已release发布并逐步稳定,众多开源库开始基于androidX进行迭代,Android在Android Studio 3.5上默认将androidX开启,以及后续新API适配等多方因素,微店APP也开始着手迁移工作了。网上博文众多,但实际给出解决迁移效率和迁移质量的方案较少,本文给出微店迁移过程开发的两款功能插件。\n<!-- more -->\n# 迁移效率插件\n\nAndroid官方提供的迁移方案,操作不复杂,在讲操作前,我们先关注以下官网的辅助文档:\n\n1.Migrating to AndroidX Overview: [https://developer.android.com/jetpack/androidx/migrate](https://developer.android.com/jetpack/androidx/migrate) \n2.Migrating to AndroidX artifact-mappings:[https://developer.android.com/jetpack/androidx/migrate/artifact-mappings](https://developer.android.com/jetpack/androidx/migrate/artifact-mappings) \n3.Migrating to AndroidX class-mappings:[https://developer.android.com/jetpack/androidx/migrate/class-mappings](https://developer.android.com/jetpack/androidx/migrate/class-mappings) \n\n上以三个文档很重要,是迁移过程我们人工修正问题的重要依据。本文简单介绍下官方建议的旧项目源码如何迁移。\n\n* 升级编译依赖\n```\n1.修改项目build.gradle中android gradle插件至3.2.0+,例如3.5.2;\n2.并且将gradle-wrapper.properties 中的gradle版本改为4.6+,例如5.6.2;\n3.升级app compileSdkVersion到 28+,例如29;\n```\n\n* 修改项目`gradle.properties`,用以支持androidx\n```\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=true\n```\n* 用Android Studio `Migrate to Androidx` 功能自动修改代码\n```\n操作路径:工具栏->Refactor->Migrate to Androidx...\n```\n\n经过以上步骤后,如果项目能编译成功并且layout布局中所有类都能找到,那恭喜你,本文下面的内容你无需查看了。我们APP尝试迁移出现了以下androidX带来的问题:\n```\n1. support替换androidx相对应类错误,这种错误会同时在java文件和布局文件中,例如android.support.v4.view.ViewPager没有正确替换成androidx.viewpager.widget.ViewPager,而是修改成了不存在的类androidx.core.view.ViewPager,导致迁移失败; \n2. java文件中相关support类import审明并未完全删除,导致迁移失败; \n3. 如果项目二次开发了相关support类或用到了只限制在support lib用的类,那么这一部分类会转换成androdx相关类失败,导致迁移失败; \n4. 可能的资源引用,在混淆时会报错,但这类问题不是致命问题,可以进行dontwarn处理;\n```\n\n官方迁移方案显然不符合我们团队以下需求: \n- APP项目是通过repo管理,子项目众多,逐个迁移效率太低; \n- Android Studio `Migrate to Androidx` 功能较弱; \n\n多方考虑,查阅`Migrate to Androidx` 功能源码后,决定自行研发迁移插件[EasyMigrateAndroidX](https://github.com/emile2013/EasyMigrateAndroidX),其使用简单执行 `./gradlew migrateAndroidX`就能准确替换内容:\n```\nEasyMigrateAndroidX原理是解析migrate.xml文件(来自AS源码),遍历所有项目(setting.gradle中include的所有项目中的类文件、资源文件以及gradle文件,并进行内容替换,能加快像repo管理或多项目迁移速度;\n```\n\n# 迁移质量插件\n\n项目源码自动修改后,还是担心会带来线上问题,JAVA源码错误较为直观,编译过程就能给出提示,但像layout文件里如有错误类,不易察觉。考虑以上场景,研发了检测布局文件中不存在的类功能插件[ResMonitor](https://github.com/emile2013/ResMonitor),执行`./gradlew checkReleaseRes`即可输出错误文件,输出内容如:\n```\nExecution failed for task ':app:checkReleaseRes'.\n> java.lang.Exception: androidx.core.view.ViewPager not exist !! but declare at:\n # Referenced at /ResMonitor/sample/app/src/main/res/layout/content_main.xml:11\n```\n其实现原理:\n```\n原理是拿到layout和manifest xml导出的混淆文件,再通过javassist检测类是否存在\n```\n当然[ResMonitor](https://github.com/emile2013/ResMonitor)不仅限于androidX迁移检测,后续可辅助上线前质量测试。\n\n# 结束语\n\nandroidX迁移,AGP是如何通过enableJetifier参数实现编译期类替换的,这块有时间再另起一博文细说。另外,小伙伴们,快去踩坑吧。\n","source":"_posts/介绍两款androidX迁移利器.md","raw":"---\ntitle: 介绍两款androidX迁移利器\ndate: 2019-12-12 19:23:57\ncategories: \n - Android\ntags: \n - 小功能组\n---\n# 背景\nsupport迁移至androidX相关包,绝大部分团队万不得已才处理,因其可见的收益过小,反而带来更大研发和测试成本。 \n考虑androidX相关包已release发布并逐步稳定,众多开源库开始基于androidX进行迭代,Android在Android Studio 3.5上默认将androidX开启,以及后续新API适配等多方因素,微店APP也开始着手迁移工作了。网上博文众多,但实际给出解决迁移效率和迁移质量的方案较少,本文给出微店迁移过程开发的两款功能插件。\n<!-- more -->\n# 迁移效率插件\n\nAndroid官方提供的迁移方案,操作不复杂,在讲操作前,我们先关注以下官网的辅助文档:\n\n1.Migrating to AndroidX Overview: [https://developer.android.com/jetpack/androidx/migrate](https://developer.android.com/jetpack/androidx/migrate) \n2.Migrating to AndroidX artifact-mappings:[https://developer.android.com/jetpack/androidx/migrate/artifact-mappings](https://developer.android.com/jetpack/androidx/migrate/artifact-mappings) \n3.Migrating to AndroidX class-mappings:[https://developer.android.com/jetpack/androidx/migrate/class-mappings](https://developer.android.com/jetpack/androidx/migrate/class-mappings) \n\n上以三个文档很重要,是迁移过程我们人工修正问题的重要依据。本文简单介绍下官方建议的旧项目源码如何迁移。\n\n* 升级编译依赖\n```\n1.修改项目build.gradle中android gradle插件至3.2.0+,例如3.5.2;\n2.并且将gradle-wrapper.properties 中的gradle版本改为4.6+,例如5.6.2;\n3.升级app compileSdkVersion到 28+,例如29;\n```\n\n* 修改项目`gradle.properties`,用以支持androidx\n```\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Automatically convert third-party libraries to use AndroidX\nandroid.enableJetifier=true\n```\n* 用Android Studio `Migrate to Androidx` 功能自动修改代码\n```\n操作路径:工具栏->Refactor->Migrate to Androidx...\n```\n\n经过以上步骤后,如果项目能编译成功并且layout布局中所有类都能找到,那恭喜你,本文下面的内容你无需查看了。我们APP尝试迁移出现了以下androidX带来的问题:\n```\n1. support替换androidx相对应类错误,这种错误会同时在java文件和布局文件中,例如android.support.v4.view.ViewPager没有正确替换成androidx.viewpager.widget.ViewPager,而是修改成了不存在的类androidx.core.view.ViewPager,导致迁移失败; \n2. java文件中相关support类import审明并未完全删除,导致迁移失败; \n3. 如果项目二次开发了相关support类或用到了只限制在support lib用的类,那么这一部分类会转换成androdx相关类失败,导致迁移失败; \n4. 可能的资源引用,在混淆时会报错,但这类问题不是致命问题,可以进行dontwarn处理;\n```\n\n官方迁移方案显然不符合我们团队以下需求: \n- APP项目是通过repo管理,子项目众多,逐个迁移效率太低; \n- Android Studio `Migrate to Androidx` 功能较弱; \n\n多方考虑,查阅`Migrate to Androidx` 功能源码后,决定自行研发迁移插件[EasyMigrateAndroidX](https://github.com/emile2013/EasyMigrateAndroidX),其使用简单执行 `./gradlew migrateAndroidX`就能准确替换内容:\n```\nEasyMigrateAndroidX原理是解析migrate.xml文件(来自AS源码),遍历所有项目(setting.gradle中include的所有项目中的类文件、资源文件以及gradle文件,并进行内容替换,能加快像repo管理或多项目迁移速度;\n```\n\n# 迁移质量插件\n\n项目源码自动修改后,还是担心会带来线上问题,JAVA源码错误较为直观,编译过程就能给出提示,但像layout文件里如有错误类,不易察觉。考虑以上场景,研发了检测布局文件中不存在的类功能插件[ResMonitor](https://github.com/emile2013/ResMonitor),执行`./gradlew checkReleaseRes`即可输出错误文件,输出内容如:\n```\nExecution failed for task ':app:checkReleaseRes'.\n> java.lang.Exception: androidx.core.view.ViewPager not exist !! but declare at:\n # Referenced at /ResMonitor/sample/app/src/main/res/layout/content_main.xml:11\n```\n其实现原理:\n```\n原理是拿到layout和manifest xml导出的混淆文件,再通过javassist检测类是否存在\n```\n当然[ResMonitor](https://github.com/emile2013/ResMonitor)不仅限于androidX迁移检测,后续可辅助上线前质量测试。\n\n# 结束语\n\nandroidX迁移,AGP是如何通过enableJetifier参数实现编译期类替换的,这块有时间再另起一博文细说。另外,小伙伴们,快去踩坑吧。\n","slug":"介绍两款androidX迁移利器","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n880009cate63cja1w5","content":"<h1 id=\"背景\"><a href=\"#背景\" class=\"headerlink\" title=\"背景\"></a>背景</h1><p>support迁移至androidX相关包,绝大部分团队万不得已才处理,因其可见的收益过小,反而带来更大研发和测试成本。<br>考虑androidX相关包已release发布并逐步稳定,众多开源库开始基于androidX进行迭代,Android在Android Studio 3.5上默认将androidX开启,以及后续新API适配等多方因素,微店APP也开始着手迁移工作了。网上博文众多,但实际给出解决迁移效率和迁移质量的方案较少,本文给出微店迁移过程开发的两款功能插件。</p>\n<span id=\"more\"></span>\n<h1 id=\"迁移效率插件\"><a href=\"#迁移效率插件\" class=\"headerlink\" title=\"迁移效率插件\"></a>迁移效率插件</h1><p>Android官方提供的迁移方案,操作不复杂,在讲操作前,我们先关注以下官网的辅助文档:</p>\n<p>1.Migrating to AndroidX Overview: <a href=\"https://developer.android.com/jetpack/androidx/migrate\">https://developer.android.com/jetpack/androidx/migrate</a><br>2.Migrating to AndroidX artifact-mappings:<a href=\"https://developer.android.com/jetpack/androidx/migrate/artifact-mappings\">https://developer.android.com/jetpack/androidx/migrate/artifact-mappings</a><br>3.Migrating to AndroidX class-mappings:<a href=\"https://developer.android.com/jetpack/androidx/migrate/class-mappings\">https://developer.android.com/jetpack/androidx/migrate/class-mappings</a> </p>\n<p>上以三个文档很重要,是迁移过程我们人工修正问题的重要依据。本文简单介绍下官方建议的旧项目源码如何迁移。</p>\n<ul>\n<li>升级编译依赖</li>\n</ul>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">1.修改项目build.gradle中android gradle插件至3.2.0+,例如3.5.2;</span><br><span class=\"line\">2.并且将gradle-wrapper.properties 中的gradle版本改为4.6+,例如5.6.2;</span><br><span class=\"line\">3.升级app compileSdkVersion到 28+,例如29;</span><br></pre></td></tr></table></figure>\n\n<ul>\n<li>修改项目<code>gradle.properties</code>,用以支持androidx</li>\n</ul>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"># When configured, Gradle will run in incubating parallel mode.</span><br><span class=\"line\"># This option should only be used with decoupled projects. More details, visit</span><br><span class=\"line\"># http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects</span><br><span class=\"line\"># org.gradle.parallel=true</span><br><span class=\"line\"># AndroidX package structure to make it clearer which packages are bundled with the</span><br><span class=\"line\"># Android operating system, and which are packaged with your app's APK</span><br><span class=\"line\"># https://developer.android.com/topic/libraries/support-library/androidx-rn</span><br><span class=\"line\">android.useAndroidX=true</span><br><span class=\"line\"># Automatically convert third-party libraries to use AndroidX</span><br><span class=\"line\">android.enableJetifier=true</span><br></pre></td></tr></table></figure>\n<ul>\n<li>用Android Studio <code>Migrate to Androidx</code> 功能自动修改代码</li>\n</ul>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">操作路径:工具栏->Refactor->Migrate to Androidx...</span><br></pre></td></tr></table></figure>\n\n<p>经过以上步骤后,如果项目能编译成功并且layout布局中所有类都能找到,那恭喜你,本文下面的内容你无需查看了。我们APP尝试迁移出现了以下androidX带来的问题:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">1. support替换androidx相对应类错误,这种错误会同时在java文件和布局文件中,例如android.support.v4.view.ViewPager没有正确替换成androidx.viewpager.widget.ViewPager,而是修改成了不存在的类androidx.core.view.ViewPager,导致迁移失败; </span><br><span class=\"line\">2. java文件中相关support类import审明并未完全删除,导致迁移失败; </span><br><span class=\"line\">3. 如果项目二次开发了相关support类或用到了只限制在support lib用的类,那么这一部分类会转换成androdx相关类失败,导致迁移失败; </span><br><span class=\"line\">4. 可能的资源引用,在混淆时会报错,但这类问题不是致命问题,可以进行dontwarn处理;</span><br></pre></td></tr></table></figure>\n\n<p>官方迁移方案显然不符合我们团队以下需求: </p>\n<ul>\n<li>APP项目是通过repo管理,子项目众多,逐个迁移效率太低; </li>\n<li>Android Studio <code>Migrate to Androidx</code> 功能较弱;</li>\n</ul>\n<p>多方考虑,查阅<code>Migrate to Androidx</code> 功能源码后,决定自行研发迁移插件<a href=\"https://github.com/emile2013/EasyMigrateAndroidX\">EasyMigrateAndroidX</a>,其使用简单执行 <code>./gradlew migrateAndroidX</code>就能准确替换内容:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">EasyMigrateAndroidX原理是解析migrate.xml文件(来自AS源码),遍历所有项目(setting.gradle中include的所有项目中的类文件、资源文件以及gradle文件,并进行内容替换,能加快像repo管理或多项目迁移速度;</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"迁移质量插件\"><a href=\"#迁移质量插件\" class=\"headerlink\" title=\"迁移质量插件\"></a>迁移质量插件</h1><p>项目源码自动修改后,还是担心会带来线上问题,JAVA源码错误较为直观,编译过程就能给出提示,但像layout文件里如有错误类,不易察觉。考虑以上场景,研发了检测布局文件中不存在的类功能插件<a href=\"https://github.com/emile2013/ResMonitor\">ResMonitor</a>,执行<code>./gradlew checkReleaseRes</code>即可输出错误文件,输出内容如:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">Execution failed for task ':app:checkReleaseRes'.</span><br><span class=\"line\">> java.lang.Exception: androidx.core.view.ViewPager not exist !! but declare at:</span><br><span class=\"line\"> # Referenced at /ResMonitor/sample/app/src/main/res/layout/content_main.xml:11</span><br></pre></td></tr></table></figure>\n<p>其实现原理:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">原理是拿到layout和manifest xml导出的混淆文件,再通过javassist检测类是否存在</span><br></pre></td></tr></table></figure>\n<p>当然<a href=\"https://github.com/emile2013/ResMonitor\">ResMonitor</a>不仅限于androidX迁移检测,后续可辅助上线前质量测试。</p>\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>androidX迁移,AGP是如何通过enableJetifier参数实现编译期类替换的,这块有时间再另起一博文细说。另外,小伙伴们,快去踩坑吧。</p>\n","excerpt":"<h1 id=\"背景\"><a href=\"#背景\" class=\"headerlink\" title=\"背景\"></a>背景</h1><p>support迁移至androidX相关包,绝大部分团队万不得已才处理,因其可见的收益过小,反而带来更大研发和测试成本。<br>考虑androidX相关包已release发布并逐步稳定,众多开源库开始基于androidX进行迭代,Android在Android Studio 3.5上默认将androidX开启,以及后续新API适配等多方因素,微店APP也开始着手迁移工作了。网上博文众多,但实际给出解决迁移效率和迁移质量的方案较少,本文给出微店迁移过程开发的两款功能插件。</p>","more":"<h1 id=\"迁移效率插件\"><a href=\"#迁移效率插件\" class=\"headerlink\" title=\"迁移效率插件\"></a>迁移效率插件</h1><p>Android官方提供的迁移方案,操作不复杂,在讲操作前,我们先关注以下官网的辅助文档:</p>\n<p>1.Migrating to AndroidX Overview: <a href=\"https://developer.android.com/jetpack/androidx/migrate\">https://developer.android.com/jetpack/androidx/migrate</a><br>2.Migrating to AndroidX artifact-mappings:<a href=\"https://developer.android.com/jetpack/androidx/migrate/artifact-mappings\">https://developer.android.com/jetpack/androidx/migrate/artifact-mappings</a><br>3.Migrating to AndroidX class-mappings:<a href=\"https://developer.android.com/jetpack/androidx/migrate/class-mappings\">https://developer.android.com/jetpack/androidx/migrate/class-mappings</a> </p>\n<p>上以三个文档很重要,是迁移过程我们人工修正问题的重要依据。本文简单介绍下官方建议的旧项目源码如何迁移。</p>\n<ul>\n<li>升级编译依赖</li>\n</ul>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">1.修改项目build.gradle中android gradle插件至3.2.0+,例如3.5.2;</span><br><span class=\"line\">2.并且将gradle-wrapper.properties 中的gradle版本改为4.6+,例如5.6.2;</span><br><span class=\"line\">3.升级app compileSdkVersion到 28+,例如29;</span><br></pre></td></tr></table></figure>\n\n<ul>\n<li>修改项目<code>gradle.properties</code>,用以支持androidx</li>\n</ul>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"># When configured, Gradle will run in incubating parallel mode.</span><br><span class=\"line\"># This option should only be used with decoupled projects. More details, visit</span><br><span class=\"line\"># http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects</span><br><span class=\"line\"># org.gradle.parallel=true</span><br><span class=\"line\"># AndroidX package structure to make it clearer which packages are bundled with the</span><br><span class=\"line\"># Android operating system, and which are packaged with your app's APK</span><br><span class=\"line\"># https://developer.android.com/topic/libraries/support-library/androidx-rn</span><br><span class=\"line\">android.useAndroidX=true</span><br><span class=\"line\"># Automatically convert third-party libraries to use AndroidX</span><br><span class=\"line\">android.enableJetifier=true</span><br></pre></td></tr></table></figure>\n<ul>\n<li>用Android Studio <code>Migrate to Androidx</code> 功能自动修改代码</li>\n</ul>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">操作路径:工具栏->Refactor->Migrate to Androidx...</span><br></pre></td></tr></table></figure>\n\n<p>经过以上步骤后,如果项目能编译成功并且layout布局中所有类都能找到,那恭喜你,本文下面的内容你无需查看了。我们APP尝试迁移出现了以下androidX带来的问题:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">1. support替换androidx相对应类错误,这种错误会同时在java文件和布局文件中,例如android.support.v4.view.ViewPager没有正确替换成androidx.viewpager.widget.ViewPager,而是修改成了不存在的类androidx.core.view.ViewPager,导致迁移失败; </span><br><span class=\"line\">2. java文件中相关support类import审明并未完全删除,导致迁移失败; </span><br><span class=\"line\">3. 如果项目二次开发了相关support类或用到了只限制在support lib用的类,那么这一部分类会转换成androdx相关类失败,导致迁移失败; </span><br><span class=\"line\">4. 可能的资源引用,在混淆时会报错,但这类问题不是致命问题,可以进行dontwarn处理;</span><br></pre></td></tr></table></figure>\n\n<p>官方迁移方案显然不符合我们团队以下需求: </p>\n<ul>\n<li>APP项目是通过repo管理,子项目众多,逐个迁移效率太低; </li>\n<li>Android Studio <code>Migrate to Androidx</code> 功能较弱;</li>\n</ul>\n<p>多方考虑,查阅<code>Migrate to Androidx</code> 功能源码后,决定自行研发迁移插件<a href=\"https://github.com/emile2013/EasyMigrateAndroidX\">EasyMigrateAndroidX</a>,其使用简单执行 <code>./gradlew migrateAndroidX</code>就能准确替换内容:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">EasyMigrateAndroidX原理是解析migrate.xml文件(来自AS源码),遍历所有项目(setting.gradle中include的所有项目中的类文件、资源文件以及gradle文件,并进行内容替换,能加快像repo管理或多项目迁移速度;</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"迁移质量插件\"><a href=\"#迁移质量插件\" class=\"headerlink\" title=\"迁移质量插件\"></a>迁移质量插件</h1><p>项目源码自动修改后,还是担心会带来线上问题,JAVA源码错误较为直观,编译过程就能给出提示,但像layout文件里如有错误类,不易察觉。考虑以上场景,研发了检测布局文件中不存在的类功能插件<a href=\"https://github.com/emile2013/ResMonitor\">ResMonitor</a>,执行<code>./gradlew checkReleaseRes</code>即可输出错误文件,输出内容如:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">Execution failed for task ':app:checkReleaseRes'.</span><br><span class=\"line\">> java.lang.Exception: androidx.core.view.ViewPager not exist !! but declare at:</span><br><span class=\"line\"> # Referenced at /ResMonitor/sample/app/src/main/res/layout/content_main.xml:11</span><br></pre></td></tr></table></figure>\n<p>其实现原理:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">原理是拿到layout和manifest xml导出的混淆文件,再通过javassist检测类是否存在</span><br></pre></td></tr></table></figure>\n<p>当然<a href=\"https://github.com/emile2013/ResMonitor\">ResMonitor</a>不仅限于androidX迁移检测,后续可辅助上线前质量测试。</p>\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>androidX迁移,AGP是如何通过enableJetifier参数实现编译期类替换的,这块有时间再另起一博文细说。另外,小伙伴们,快去踩坑吧。</p>"},{"title":"埋点系统之可信数据采集持续集成实践","date":"2019-08-03T08:53:27.000Z","_content":"# 引言\n埋点数据,必须是准确的、完备的,否则无法为上层BI产品提供可信数据。端数据采集方案可简单归类为手动埋点、可视化埋点以及无痕埋点三大类。在高DAU APP中,要完全脱离手动埋点,实行无痕埋点方案,在网络带宽、数据存储等方面会带来一定的压力。我司目前停留在手动埋点阶段,在实践过程,我们发现,由于手动埋点依赖上层业务开发人员,也较容易出现漏埋和错埋,并且错误数据的发现存在滞后性,往往在BI输出产品阶段才被发现。 \n为保证数据的可靠性,以及尽量减少开发人员对埋点的工作投入,我们做了以下几方面工作: \n1. 在浏览器端,提供埋点管理平台,为BI、测试、开发等角色提供事件定义、埋点版本追踪、数据验收、数据回归测试等持续集成功能; \n2. 在手持端,在APP中嵌入可视化埋点工具,为开发与测试同学提供实时数据校验功能; \n3. 简化开发同学编码:控件点击、页面曝光只需一行代码;页面等事件无需开发同学关注,埋点SDK自动上报并补全设备和用户信息; \n<!-- more -->\n\n部分术语\n>埋点协议:规范一条埋点数据格式,数据对象中各字段含义以及取值来源;协议要尽量涵盖Android和iOS设备纬度信息、用户信息、以及业务自身数据; \n>埋点事件:标识一个原子操作,比如一次控件点击、一次网络请求; \n>业务埋点事件:BI或业务关心的数据,比如用户注册、控件点击,页面路径; \n>技术埋点事件:大部分是开发人员关注的数据,可用于各性能指标的统计,比如网络事件、CDN健康状态事件; \n\n# 埋点管理平台\n埋点管理平台承载以下基础功能:\n1、规范和约束事件定义; \n2、业务埋点持续集成,BI编辑埋点、开发查看变更埋点和测试人工/自动化测试埋点; \n3、业务埋点测试辅助,如埋点回归验证; \n4、基于规范化埋点管理,输出数据相关服务,如 kibana自动化查询业务数据需求; \n\n本文我们只关注,如果让数据开发与验收成为持续集成中不可或缺的一环。\n## 埋点流程\n\n在APP版本研发前,产品同学会提出需求,以及他们所关心的数据。BI同学会与产品同学沟通,输出必要的业务埋点事件,同时录入埋点管理平台,如下图:\n \n埋点管理平台,提供埋点查询与过滤功能。例如,我们提供APP版本、开发同学归属的埋点、埋点所处页面,区块等过滤条件;我们用颜色区分埋点事件的状态,如红色标识为变更埋点,黄色标识为新增埋点等。通过各维度查询,尽量让开发和测试同学快速找到关心的埋点数据。\n \n开发同学收到埋点通知后,进行埋点开发,自测通过后在埋点管理平台进行开发状态确认。测试同学收到埋点开发完成通知,进行埋点测试与校验。\n \n通过埋点录入、埋点开发和校验,能极大减少漏埋和错埋的案例发生。\n\n# 手持端数据校验\n\n通过埋点管理平台,也无法完全杜绝漏埋和错埋,比如特殊场景下,es数据消费阻塞,开发和测试同学无法在浏览器端进行校验,这时就要在手持端进行校验工作了。\n\n手持端可视化埋点工具,要做到不能阻塞APP操作,以及要能实时显示埋点数据和数据汇总查询。借助[hyperion](https://github.com/hyperion-project)以及埋点SDK提供的功能,我们实现了最初的想法。部分截图如下: \n \n \n \n# 结束语\n\n埋点这块,有很多可以和大家讨论的点,比如埋点SDK上报流程设计、前端与后台数据存储、实时和离线数据分析与利用。本文只是一个起点,后续会陆续分享出来我们现在的一些玩法,都是我们的一些实践。\n\n\n\n","source":"_posts/埋点系统之可信数据持续集成实践.md","raw":"---\ntitle: 埋点系统之可信数据采集持续集成实践\ndate: 2019-08-03 16:53:27\ncategories: \n - Android\ntags: \n - 埋点系统\n---\n# 引言\n埋点数据,必须是准确的、完备的,否则无法为上层BI产品提供可信数据。端数据采集方案可简单归类为手动埋点、可视化埋点以及无痕埋点三大类。在高DAU APP中,要完全脱离手动埋点,实行无痕埋点方案,在网络带宽、数据存储等方面会带来一定的压力。我司目前停留在手动埋点阶段,在实践过程,我们发现,由于手动埋点依赖上层业务开发人员,也较容易出现漏埋和错埋,并且错误数据的发现存在滞后性,往往在BI输出产品阶段才被发现。 \n为保证数据的可靠性,以及尽量减少开发人员对埋点的工作投入,我们做了以下几方面工作: \n1. 在浏览器端,提供埋点管理平台,为BI、测试、开发等角色提供事件定义、埋点版本追踪、数据验收、数据回归测试等持续集成功能; \n2. 在手持端,在APP中嵌入可视化埋点工具,为开发与测试同学提供实时数据校验功能; \n3. 简化开发同学编码:控件点击、页面曝光只需一行代码;页面等事件无需开发同学关注,埋点SDK自动上报并补全设备和用户信息; \n<!-- more -->\n\n部分术语\n>埋点协议:规范一条埋点数据格式,数据对象中各字段含义以及取值来源;协议要尽量涵盖Android和iOS设备纬度信息、用户信息、以及业务自身数据; \n>埋点事件:标识一个原子操作,比如一次控件点击、一次网络请求; \n>业务埋点事件:BI或业务关心的数据,比如用户注册、控件点击,页面路径; \n>技术埋点事件:大部分是开发人员关注的数据,可用于各性能指标的统计,比如网络事件、CDN健康状态事件; \n\n# 埋点管理平台\n埋点管理平台承载以下基础功能:\n1、规范和约束事件定义; \n2、业务埋点持续集成,BI编辑埋点、开发查看变更埋点和测试人工/自动化测试埋点; \n3、业务埋点测试辅助,如埋点回归验证; \n4、基于规范化埋点管理,输出数据相关服务,如 kibana自动化查询业务数据需求; \n\n本文我们只关注,如果让数据开发与验收成为持续集成中不可或缺的一环。\n## 埋点流程\n\n在APP版本研发前,产品同学会提出需求,以及他们所关心的数据。BI同学会与产品同学沟通,输出必要的业务埋点事件,同时录入埋点管理平台,如下图:\n \n埋点管理平台,提供埋点查询与过滤功能。例如,我们提供APP版本、开发同学归属的埋点、埋点所处页面,区块等过滤条件;我们用颜色区分埋点事件的状态,如红色标识为变更埋点,黄色标识为新增埋点等。通过各维度查询,尽量让开发和测试同学快速找到关心的埋点数据。\n \n开发同学收到埋点通知后,进行埋点开发,自测通过后在埋点管理平台进行开发状态确认。测试同学收到埋点开发完成通知,进行埋点测试与校验。\n \n通过埋点录入、埋点开发和校验,能极大减少漏埋和错埋的案例发生。\n\n# 手持端数据校验\n\n通过埋点管理平台,也无法完全杜绝漏埋和错埋,比如特殊场景下,es数据消费阻塞,开发和测试同学无法在浏览器端进行校验,这时就要在手持端进行校验工作了。\n\n手持端可视化埋点工具,要做到不能阻塞APP操作,以及要能实时显示埋点数据和数据汇总查询。借助[hyperion](https://github.com/hyperion-project)以及埋点SDK提供的功能,我们实现了最初的想法。部分截图如下: \n \n \n \n# 结束语\n\n埋点这块,有很多可以和大家讨论的点,比如埋点SDK上报流程设计、前端与后台数据存储、实时和离线数据分析与利用。本文只是一个起点,后续会陆续分享出来我们现在的一些玩法,都是我们的一些实践。\n\n\n\n","slug":"埋点系统之可信数据持续集成实践","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n89000dcatea0l46yoh","content":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>埋点数据,必须是准确的、完备的,否则无法为上层BI产品提供可信数据。端数据采集方案可简单归类为手动埋点、可视化埋点以及无痕埋点三大类。在高DAU APP中,要完全脱离手动埋点,实行无痕埋点方案,在网络带宽、数据存储等方面会带来一定的压力。我司目前停留在手动埋点阶段,在实践过程,我们发现,由于手动埋点依赖上层业务开发人员,也较容易出现漏埋和错埋,并且错误数据的发现存在滞后性,往往在BI输出产品阶段才被发现。<br>为保证数据的可靠性,以及尽量减少开发人员对埋点的工作投入,我们做了以下几方面工作: </p>\n<ol>\n<li>在浏览器端,提供埋点管理平台,为BI、测试、开发等角色提供事件定义、埋点版本追踪、数据验收、数据回归测试等持续集成功能; </li>\n<li>在手持端,在APP中嵌入可视化埋点工具,为开发与测试同学提供实时数据校验功能; </li>\n<li>简化开发同学编码:控件点击、页面曝光只需一行代码;页面等事件无需开发同学关注,埋点SDK自动上报并补全设备和用户信息;</li>\n</ol>\n<span id=\"more\"></span>\n\n<p>部分术语</p>\n<blockquote>\n<p>埋点协议:规范一条埋点数据格式,数据对象中各字段含义以及取值来源;协议要尽量涵盖Android和iOS设备纬度信息、用户信息、以及业务自身数据;<br>埋点事件:标识一个原子操作,比如一次控件点击、一次网络请求;<br>业务埋点事件:BI或业务关心的数据,比如用户注册、控件点击,页面路径;<br>技术埋点事件:大部分是开发人员关注的数据,可用于各性能指标的统计,比如网络事件、CDN健康状态事件; </p>\n</blockquote>\n<h1 id=\"埋点管理平台\"><a href=\"#埋点管理平台\" class=\"headerlink\" title=\"埋点管理平台\"></a>埋点管理平台</h1><p>埋点管理平台承载以下基础功能:<br>1、规范和约束事件定义;<br>2、业务埋点持续集成,BI编辑埋点、开发查看变更埋点和测试人工/自动化测试埋点;<br>3、业务埋点测试辅助,如埋点回归验证;<br>4、基于规范化埋点管理,输出数据相关服务,如 kibana自动化查询业务数据需求; </p>\n<p>本文我们只关注,如果让数据开发与验收成为持续集成中不可或缺的一环。</p>\n<h2 id=\"埋点流程\"><a href=\"#埋点流程\" class=\"headerlink\" title=\"埋点流程\"></a>埋点流程</h2><p>在APP版本研发前,产品同学会提出需求,以及他们所关心的数据。BI同学会与产品同学沟通,输出必要的业务埋点事件,同时录入埋点管理平台,如下图:<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/splash.png?raw=true\"><br>埋点管理平台,提供埋点查询与过滤功能。例如,我们提供APP版本、开发同学归属的埋点、埋点所处页面,区块等过滤条件;我们用颜色区分埋点事件的状态,如红色标识为变更埋点,黄色标识为新增埋点等。通过各维度查询,尽量让开发和测试同学快速找到关心的埋点数据。<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/filter.png?raw=true\"><br>开发同学收到埋点通知后,进行埋点开发,自测通过后在埋点管理平台进行开发状态确认。测试同学收到埋点开发完成通知,进行埋点测试与校验。<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/state.png?raw=true\"><br>通过埋点录入、埋点开发和校验,能极大减少漏埋和错埋的案例发生。</p>\n<h1 id=\"手持端数据校验\"><a href=\"#手持端数据校验\" class=\"headerlink\" title=\"手持端数据校验\"></a>手持端数据校验</h1><p>通过埋点管理平台,也无法完全杜绝漏埋和错埋,比如特殊场景下,es数据消费阻塞,开发和测试同学无法在浏览器端进行校验,这时就要在手持端进行校验工作了。</p>\n<p>手持端可视化埋点工具,要做到不能阻塞APP操作,以及要能实时显示埋点数据和数据汇总查询。借助<a href=\"https://github.com/hyperion-project\">hyperion</a>以及埋点SDK提供的功能,我们实现了最初的想法。部分截图如下:<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/android1.png?raw=true\"><br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/android2.png?raw=true\"><br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/android3.png?raw=true\"> </p>\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>埋点这块,有很多可以和大家讨论的点,比如埋点SDK上报流程设计、前端与后台数据存储、实时和离线数据分析与利用。本文只是一个起点,后续会陆续分享出来我们现在的一些玩法,都是我们的一些实践。</p>\n","excerpt":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>埋点数据,必须是准确的、完备的,否则无法为上层BI产品提供可信数据。端数据采集方案可简单归类为手动埋点、可视化埋点以及无痕埋点三大类。在高DAU APP中,要完全脱离手动埋点,实行无痕埋点方案,在网络带宽、数据存储等方面会带来一定的压力。我司目前停留在手动埋点阶段,在实践过程,我们发现,由于手动埋点依赖上层业务开发人员,也较容易出现漏埋和错埋,并且错误数据的发现存在滞后性,往往在BI输出产品阶段才被发现。<br>为保证数据的可靠性,以及尽量减少开发人员对埋点的工作投入,我们做了以下几方面工作: </p>\n<ol>\n<li>在浏览器端,提供埋点管理平台,为BI、测试、开发等角色提供事件定义、埋点版本追踪、数据验收、数据回归测试等持续集成功能; </li>\n<li>在手持端,在APP中嵌入可视化埋点工具,为开发与测试同学提供实时数据校验功能; </li>\n<li>简化开发同学编码:控件点击、页面曝光只需一行代码;页面等事件无需开发同学关注,埋点SDK自动上报并补全设备和用户信息;</li>\n</ol>","more":"<p>部分术语</p>\n<blockquote>\n<p>埋点协议:规范一条埋点数据格式,数据对象中各字段含义以及取值来源;协议要尽量涵盖Android和iOS设备纬度信息、用户信息、以及业务自身数据;<br>埋点事件:标识一个原子操作,比如一次控件点击、一次网络请求;<br>业务埋点事件:BI或业务关心的数据,比如用户注册、控件点击,页面路径;<br>技术埋点事件:大部分是开发人员关注的数据,可用于各性能指标的统计,比如网络事件、CDN健康状态事件; </p>\n</blockquote>\n<h1 id=\"埋点管理平台\"><a href=\"#埋点管理平台\" class=\"headerlink\" title=\"埋点管理平台\"></a>埋点管理平台</h1><p>埋点管理平台承载以下基础功能:<br>1、规范和约束事件定义;<br>2、业务埋点持续集成,BI编辑埋点、开发查看变更埋点和测试人工/自动化测试埋点;<br>3、业务埋点测试辅助,如埋点回归验证;<br>4、基于规范化埋点管理,输出数据相关服务,如 kibana自动化查询业务数据需求; </p>\n<p>本文我们只关注,如果让数据开发与验收成为持续集成中不可或缺的一环。</p>\n<h2 id=\"埋点流程\"><a href=\"#埋点流程\" class=\"headerlink\" title=\"埋点流程\"></a>埋点流程</h2><p>在APP版本研发前,产品同学会提出需求,以及他们所关心的数据。BI同学会与产品同学沟通,输出必要的业务埋点事件,同时录入埋点管理平台,如下图:<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/splash.png?raw=true\"><br>埋点管理平台,提供埋点查询与过滤功能。例如,我们提供APP版本、开发同学归属的埋点、埋点所处页面,区块等过滤条件;我们用颜色区分埋点事件的状态,如红色标识为变更埋点,黄色标识为新增埋点等。通过各维度查询,尽量让开发和测试同学快速找到关心的埋点数据。<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/filter.png?raw=true\"><br>开发同学收到埋点通知后,进行埋点开发,自测通过后在埋点管理平台进行开发状态确认。测试同学收到埋点开发完成通知,进行埋点测试与校验。<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/state.png?raw=true\"><br>通过埋点录入、埋点开发和校验,能极大减少漏埋和错埋的案例发生。</p>\n<h1 id=\"手持端数据校验\"><a href=\"#手持端数据校验\" class=\"headerlink\" title=\"手持端数据校验\"></a>手持端数据校验</h1><p>通过埋点管理平台,也无法完全杜绝漏埋和错埋,比如特殊场景下,es数据消费阻塞,开发和测试同学无法在浏览器端进行校验,这时就要在手持端进行校验工作了。</p>\n<p>手持端可视化埋点工具,要做到不能阻塞APP操作,以及要能实时显示埋点数据和数据汇总查询。借助<a href=\"https://github.com/hyperion-project\">hyperion</a>以及埋点SDK提供的功能,我们实现了最初的想法。部分截图如下:<br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/android1.png?raw=true\"><br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/android2.png?raw=true\"><br><img src=\"https://github.com/emile2013/emile2013.github.io/blob/master/imgs/android3.png?raw=true\"> </p>\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>埋点这块,有很多可以和大家讨论的点,比如埋点SDK上报流程设计、前端与后台数据存储、实时和离线数据分析与利用。本文只是一个起点,后续会陆续分享出来我们现在的一些玩法,都是我们的一些实践。</p>"},{"title":"小功能之回退首页可行性解决方案探索","date":"2019-10-17T12:53:29.000Z","_content":"\n看代码更直观,链接:https://github.com/emile2013/nav2main","source":"_posts/小功能之回退首页可行性解决方案探索.md","raw":"---\ntitle: 小功能之回退首页可行性解决方案探索\ndate: 2019-10-17 20:53:29\ncategories: \n - Android\ntags: \n - 小功能组\n---\n\n看代码更直观,链接:https://github.com/emile2013/nav2main","slug":"小功能之回退首页可行性解决方案探索","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n89000ecate7iy10w9a","content":"<p>看代码更直观,链接:<a href=\"https://github.com/emile2013/nav2main\">https://github.com/emile2013/nav2main</a></p>\n","excerpt":"","more":"<p>看代码更直观,链接:<a href=\"https://github.com/emile2013/nav2main\">https://github.com/emile2013/nav2main</a></p>\n"},{"title":"插件化之AAPT客户化","date":"2019-07-19T08:01:21.000Z","_content":"# 引言\n这篇文章其实是借花献佛,微店的aapt工作,绝大部分是同事[区长](https://fucknmb.com/about/)编码和验证,文章本意在于对插件化架构中aapt相关流程的总结。\n在Android编译流程中,aapt主要处理资源相关工作。插件化架构中,需要客户化aapt,让其承担宿主导出符号表、宿主导入符号表、插件导入宿主ap_文件、 修改插件package id、简化书写插件引用宿主资源等额外功能。\n<!-- more -->\n# 宿主导出符号表\n宿主导出符号表主要用于:固定资源值;供插件调用宿主资源;aapt和aapt2实现方式不一样。\n## aapt方式\n通过aapt link -P 参数实现。更多参数可通过(aapt --help)查看。\n> -P specify where to output public resource definitions \n\n```groovy\n//DSL\nandroid {\n aaptOptions {\n additionalParameters \"-P\", \"${project.getBuildDir()}/public.xml\"\n }\n}\n\n//伪代码\n additionalParameters.add(\"-P\")\n additionalParameters.add(redirectFile.getAbsolutePath())\n processAndroidResourceTask.doLast {\n if (redirectFile.exists()) {\n exportSymbolFile.append(\"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"?>\\n\")\n exportSymbolFile.write(\"<!-- AUTO-GENERATED FILE. DO NOT MODIFY -->\\n\")\n exportSymbolFile.append(\"<resources>\\n\")\n def publicXMLNodes = new XmlParser().parse(redirectFile)\n publicXMLNodes.each {\n //这里可以有选择导出,比如只导出style和attr资源\n exportSymbolFile.append(\" <public type=\\\"${it.@type}\\\" name=\\\"${it.@name}\\\" id=\\\"${it.@id}\\\" />\\n\")\n }\n exportSymbolFile.append(\"</resources>\")\n }\n}\n```\n\n导出文件类似于:\n\n ``` \n<resources>\n <public type=\"attr\" name=\"barrierAllowsGoneWidgets\" id=\"0x7f010004\" />\n <public type=\"attr\" name=\"barrierDirection\" id=\"0x7f010005\" />\n</resources>\n ```\n\n\n## aapt2方式\n\n通过aapt2 link --emit-ids 参数实现。更多参数可查看[aapt2](https://developer.android.com/studio/command-line/aapt2#link)文档[1]。\n>--emit-ids Emits a file at the given path with a list of names of resource \n>types and their ID mappings. It is suitable to use with --stable-ids.\n>可以产生资源id文件,可以适用于--stable-ids\n\n```\n//DSL\nandroid {\n aaptOptions {\n additionalParameters \"--emit-ids\", \"${project.file('public.txt')}\"\n }\n}\n//伪代码\nadditionalParameters.add(\"--emit-ids\")\nadditionalParameters.add(redirectFile.getAbsolutePath())\nprocessAndroidResourceTask.doLast {\n if (redirectFile.exists()) {\n Pattern filterPattern = Pattern.compile(\".*?:(.*?)/.*?\")\n List<String> sortedLines = new ArrayList<>()\n redirectFile.eachLine { String line ->\n Matcher matcher = filterPattern.matcher(line)\n if (matcher.matches() && matcher.groupCount() == 1) {\n //这里可以有选择导出,比如只导出style和attr资源\n exportSymbolFile.append(\"${line}\\n\")\n }\n }\n }\n}\n```\n\n导出文件类似于:\n\n ```\ncom.koudai.weidian.buyer:attr/barrierAllowsGoneWidgets = 0x7f010004\ncom.koudai.weidian.buyer:attr/barrierDirection = 0x7f010005\n ```\n\n# 宿主导入符号表\n宿主导入符号表主要用于资源ID固定,在patch和插件化中常见。同样aapt和aapt2处理方式有所不同。\n\n## aapt方式\n\naapt与aapt2相比较为简单,我们只要在aapt link(对应 processResouceTask)前(比如 mergeResourceTask或 processManifestTask)把public.xml和ids.xml放到res目录中,参与后续的aapt.link编译就行。\n\n这里有个知识点:\n>导入public.xml时,如果public.xml里有id \n>资源,但在values.xml里不存在,要额外新建一个xml文件,在文件里把id定义补上。\n\n## aapt2方式\n\naapt2导入public.txt也较为简单,主要利用aapt2 --stable-ids 参数。\n>--emit-ids path [Emits a file at the given path with a list of names of \n>resource types and their ID mappings. It is suitable to use with--stable-ids.]\n>--stable-ids outputfilename.ext [Consumes the file generated with --emit-ids \n>containing the list of names of resource types and their assigned IDs. This \n>option allows assigned IDs to remain stable even when you delete or add new \n>resources while linking.]\n\n但在插件化架构中,仅仅导入还不行,我们还要将导入的资源打上public flag标,才能供插件引用。打public flag标较为复杂,可参考团队同事写的[aapt2 适配之资源 id 固定](https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more)[2] ,其原理是:\n> aapt2 在compile阶段就为资源打上public标,--stable-ids参数仅让资源参与link。\n\n# 插件导入宿主ap_\n插件导入宿主ap_文件,是为了能有效资源共享,在我们的插件化体系中,还有就是解决宿主合并插件Manifest后,透明主题无法动态生效等问题。\n导入ap_文件,aapt和aapt2实现一致,主要利用-I参数,让ap_文件参与aapt link执行过程,参数说明如下:\n>-I add an existing package to base include set\n\n# 修改插件资源packageid\n\n修改pacakge id,主要是指修改资源的包名id,即0xPPTTEEEE(PackageId+TypeId+EntryId)中的PP段值。在Android体系中,非系统应用,资源PP段都是0x7f。在国内ROM里,资源ID的可分配区间为[0x21,0x7f)[3],主要原因在于:\n>0x1x为系统保留 0x7f为主apk使用 0x20之前的发现miui里面内部已使用\n\n特别注意是,在联想ZUI ROM系统中,0x4f被占用。PP段固定业界实现有两种:修改二进制文件(arsc文件相关处理)和客户化aapt。二进制文件实现方式有滴滴的[VirtualAPK](https://github.com/didi/VirtualAPK/wiki),[Small](https://github.com/wequick/Small)等开源实现。我们这里仅讲解aapt实现方式。\n\n## aapt方式\n\naapt修改packageid,需要我们修改aapt源码,业界实现方式大同小异,aapt各版本改动也可能不同。我们以sdk 9的版本为例,简述如何修改,大体思路如下:\n\n>加入自定义参数-apk-module,在Main.cpp中获取其值,并在ResourceTypes.cpp中植入。\n\n```\n//Main.cpp 第一个改动点\nint main(int argc, char* const argv[])\n{\n ...\n if(strcmp(cp, \"-apk-module\") == 0){\n argc--;\n argv++;\n if (!argc) {\n fprintf(stderr, \"ERROR: No argument supplied for '--apk-module' option\\n\");\n wantUsage = true;\n goto bail;\n }\n //取得 package id, apkModuleId可以像atlas [4]一样作为一个全局变量存在或保存在一个自定义单例中\n apkModuleId = strtol(argv[0], NULL, 16);\n }\n ... \n} \n\n```\n\n```\n//ResourceTypes.cpp 第二个改动点\n...\nDynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib)\n : mAssignedPackageId(packageId)\n , mAppAsLib(appAsLib)\n{\n memset(mLookupTable, 0, sizeof(mLookupTable));\n\n // Reserved package ids\n mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID;\n mLookupTable[apkModuleId] = apkModuleId;\n mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID;\n}\n...\nbool ResTable::stringToValue(Res_value* outValue, String16* outString,\n const char16_t* s, size_t len,\n bool preserveSpaces, bool coerceType,\n uint32_t attrID,\n const String16* defType,\n const String16* defPackage,\n Accessor* accessor,\n void* accessorCookie,\n uint32_t attrType,\n bool enforcePrivate) const\n{\n\n //只要在if (packageId != APP_PACKAGE_ID && packageId != SYS_PACKAGE_ID)的条件语句中加入自定义的packageid判断就行,如下所示\n if (packageId != apkModuleId && packageId != APP_PACKAGE_ID &&\n packageId != SYS_PACKAGE_ID) {\n ... \n }\n} \n\nstatus_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {\n //与stringToValue函数处理类,相应if语句加入以下逻辑\n if ((packageId == apkModuleId || packageId == APP_PACKAGE_ID) && !mAppAsLib) {\n ...\n }\n} \n```\n\n## aapt2方式\n\naapt2原生就支持自定义PP段,比如现在的Android App Bundle就用到了此功能,其参数定义如下:\n\n>--package-id package-id [Specifies the package ID to use for your app. The \n>package ID that you specify must be greater than or equal to 0x7f unless used in combination with --allow-reserved-package-id.]\n\n但是 在sdk 6,7,8的aapt2只支持 0x7f-0xff区域的package id值,在9之后就我们添加 --allow-reserved-package-id参数就可以在0x02-0x7f区域。\n\n# 简化书写引用宿主资源\n\n插件中引用宿主资源,我们只要把宿主ap_文件参与插件编译即可。但我们在书写资源引用时,与引用android系统资源类似(@packageName:resType/resName),必须带上宿主包名,如 @xxx.xx.xx:color/white。如果我们不重写appt做相应处理,那在IDEA里会容易爆红。\n解决方法:重写aapt,让业务插件不用书写包名过程,像引用plugin自身资源一样,直接找到资源。\n## aapt方式\n与修改packageid类似,我们加入自定义\n```\n//Main.cpp 第一个改动点\nint main(int argc, char* const argv[])\n{\n ...\n if(strcmp(cp, \"-host-package\") == 0) == 0){\n argc--;\n argv++;\n if (!argc) {\n fprintf(stderr, \"ERROR: No argument supplied for '--host-package' option\\n\");\n wantUsage = true;\n goto bail;\n }\n hostPackage=std::string(argv[0]);\n }\n ... \n} \n\n```\n\n```\n//ResourceTypes.cpp 第二个改动点\nbool ResTable::stringToValue(Res_value* outValue, String16* outString,\n const char16_t* s, size_t len,\n bool preserveSpaces, bool coerceType,\n uint32_t attrID,\n const String16* defType,\n const String16* defPackage,\n Accessor* accessor,\n void* accessorCookie,\n uint32_t attrType,\n bool enforcePrivate) const\n{\n\n ...\n if (*s == '#') {\n //末尾\n if (hostPackage.length() > 0 &&\n *defPackage != String16(hostPackage.c_str())) {\n String16 hostPackageName = String16(hostPackage.c_str());\n return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,\n &hostPackageName,\n accessor, accessorCookie, attrType, enforcePrivate);\n } else {\n if (accessor != NULL) {\n accessor->reportError(accessorCookie, \"No resource found that matches the given name\");\n }\n }\n return false;\n }\n\n\n if (*s == '?') {\n //末尾\n if (hostPackage.length() > 0 &&\n *defPackage != String16(hostPackage.c_str())) {\n String16 hostPackageName = String16(hostPackage.c_str());\n return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,\n &hostPackageName,\n accessor, accessorCookie, attrType, enforcePrivate);\n } else {\n if (accessor != NULL) {\n accessor->reportError(accessorCookie, \"No resource found that matches the given name\");\n }\n }\n return false;\n }\n\n} \n```\n\n## aapt2方式\n\n与aapt类似,ResourceTypes.cpp和aapt是同个文件,不过入口函数没在Main.cpp里,而是Link.cpp。\n```\n//Link.cpp\nint Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) {\n ...\n Flags flags =\n Flags()\n .OptionalFlag(\"--host-package\",\n \"Specific host packageName for plugin refer resources from host\\n\",\n &host_package)\n ...\n} \n```\n\n# 参考\n[1] AAPT2 USER GUIDE: [https://developer.android.com/studio/command-line/aapt2#link](https://developer.android.com/studio/command-line/aapt2#link) \n[2] aapt2 适配之资源id固定:[https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more](https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more) \n[3] Atlas 中文文档手册 :[https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md](https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md) \n[4] Atlas-aapt: [https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md](https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md) ","source":"_posts/插件化之AAPT客户化.md","raw":"---\ntitle: 插件化之AAPT客户化\ndate: 2019-07-19 16:01:21\ncategories: \n - Android\ntags: \n - 插件化\n---\n# 引言\n这篇文章其实是借花献佛,微店的aapt工作,绝大部分是同事[区长](https://fucknmb.com/about/)编码和验证,文章本意在于对插件化架构中aapt相关流程的总结。\n在Android编译流程中,aapt主要处理资源相关工作。插件化架构中,需要客户化aapt,让其承担宿主导出符号表、宿主导入符号表、插件导入宿主ap_文件、 修改插件package id、简化书写插件引用宿主资源等额外功能。\n<!-- more -->\n# 宿主导出符号表\n宿主导出符号表主要用于:固定资源值;供插件调用宿主资源;aapt和aapt2实现方式不一样。\n## aapt方式\n通过aapt link -P 参数实现。更多参数可通过(aapt --help)查看。\n> -P specify where to output public resource definitions \n\n```groovy\n//DSL\nandroid {\n aaptOptions {\n additionalParameters \"-P\", \"${project.getBuildDir()}/public.xml\"\n }\n}\n\n//伪代码\n additionalParameters.add(\"-P\")\n additionalParameters.add(redirectFile.getAbsolutePath())\n processAndroidResourceTask.doLast {\n if (redirectFile.exists()) {\n exportSymbolFile.append(\"<?xml version=\\\"1.0\\\" encoding=\\\"utf-8\\\"?>\\n\")\n exportSymbolFile.write(\"<!-- AUTO-GENERATED FILE. DO NOT MODIFY -->\\n\")\n exportSymbolFile.append(\"<resources>\\n\")\n def publicXMLNodes = new XmlParser().parse(redirectFile)\n publicXMLNodes.each {\n //这里可以有选择导出,比如只导出style和attr资源\n exportSymbolFile.append(\" <public type=\\\"${it.@type}\\\" name=\\\"${it.@name}\\\" id=\\\"${it.@id}\\\" />\\n\")\n }\n exportSymbolFile.append(\"</resources>\")\n }\n}\n```\n\n导出文件类似于:\n\n ``` \n<resources>\n <public type=\"attr\" name=\"barrierAllowsGoneWidgets\" id=\"0x7f010004\" />\n <public type=\"attr\" name=\"barrierDirection\" id=\"0x7f010005\" />\n</resources>\n ```\n\n\n## aapt2方式\n\n通过aapt2 link --emit-ids 参数实现。更多参数可查看[aapt2](https://developer.android.com/studio/command-line/aapt2#link)文档[1]。\n>--emit-ids Emits a file at the given path with a list of names of resource \n>types and their ID mappings. It is suitable to use with --stable-ids.\n>可以产生资源id文件,可以适用于--stable-ids\n\n```\n//DSL\nandroid {\n aaptOptions {\n additionalParameters \"--emit-ids\", \"${project.file('public.txt')}\"\n }\n}\n//伪代码\nadditionalParameters.add(\"--emit-ids\")\nadditionalParameters.add(redirectFile.getAbsolutePath())\nprocessAndroidResourceTask.doLast {\n if (redirectFile.exists()) {\n Pattern filterPattern = Pattern.compile(\".*?:(.*?)/.*?\")\n List<String> sortedLines = new ArrayList<>()\n redirectFile.eachLine { String line ->\n Matcher matcher = filterPattern.matcher(line)\n if (matcher.matches() && matcher.groupCount() == 1) {\n //这里可以有选择导出,比如只导出style和attr资源\n exportSymbolFile.append(\"${line}\\n\")\n }\n }\n }\n}\n```\n\n导出文件类似于:\n\n ```\ncom.koudai.weidian.buyer:attr/barrierAllowsGoneWidgets = 0x7f010004\ncom.koudai.weidian.buyer:attr/barrierDirection = 0x7f010005\n ```\n\n# 宿主导入符号表\n宿主导入符号表主要用于资源ID固定,在patch和插件化中常见。同样aapt和aapt2处理方式有所不同。\n\n## aapt方式\n\naapt与aapt2相比较为简单,我们只要在aapt link(对应 processResouceTask)前(比如 mergeResourceTask或 processManifestTask)把public.xml和ids.xml放到res目录中,参与后续的aapt.link编译就行。\n\n这里有个知识点:\n>导入public.xml时,如果public.xml里有id \n>资源,但在values.xml里不存在,要额外新建一个xml文件,在文件里把id定义补上。\n\n## aapt2方式\n\naapt2导入public.txt也较为简单,主要利用aapt2 --stable-ids 参数。\n>--emit-ids path [Emits a file at the given path with a list of names of \n>resource types and their ID mappings. It is suitable to use with--stable-ids.]\n>--stable-ids outputfilename.ext [Consumes the file generated with --emit-ids \n>containing the list of names of resource types and their assigned IDs. This \n>option allows assigned IDs to remain stable even when you delete or add new \n>resources while linking.]\n\n但在插件化架构中,仅仅导入还不行,我们还要将导入的资源打上public flag标,才能供插件引用。打public flag标较为复杂,可参考团队同事写的[aapt2 适配之资源 id 固定](https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more)[2] ,其原理是:\n> aapt2 在compile阶段就为资源打上public标,--stable-ids参数仅让资源参与link。\n\n# 插件导入宿主ap_\n插件导入宿主ap_文件,是为了能有效资源共享,在我们的插件化体系中,还有就是解决宿主合并插件Manifest后,透明主题无法动态生效等问题。\n导入ap_文件,aapt和aapt2实现一致,主要利用-I参数,让ap_文件参与aapt link执行过程,参数说明如下:\n>-I add an existing package to base include set\n\n# 修改插件资源packageid\n\n修改pacakge id,主要是指修改资源的包名id,即0xPPTTEEEE(PackageId+TypeId+EntryId)中的PP段值。在Android体系中,非系统应用,资源PP段都是0x7f。在国内ROM里,资源ID的可分配区间为[0x21,0x7f)[3],主要原因在于:\n>0x1x为系统保留 0x7f为主apk使用 0x20之前的发现miui里面内部已使用\n\n特别注意是,在联想ZUI ROM系统中,0x4f被占用。PP段固定业界实现有两种:修改二进制文件(arsc文件相关处理)和客户化aapt。二进制文件实现方式有滴滴的[VirtualAPK](https://github.com/didi/VirtualAPK/wiki),[Small](https://github.com/wequick/Small)等开源实现。我们这里仅讲解aapt实现方式。\n\n## aapt方式\n\naapt修改packageid,需要我们修改aapt源码,业界实现方式大同小异,aapt各版本改动也可能不同。我们以sdk 9的版本为例,简述如何修改,大体思路如下:\n\n>加入自定义参数-apk-module,在Main.cpp中获取其值,并在ResourceTypes.cpp中植入。\n\n```\n//Main.cpp 第一个改动点\nint main(int argc, char* const argv[])\n{\n ...\n if(strcmp(cp, \"-apk-module\") == 0){\n argc--;\n argv++;\n if (!argc) {\n fprintf(stderr, \"ERROR: No argument supplied for '--apk-module' option\\n\");\n wantUsage = true;\n goto bail;\n }\n //取得 package id, apkModuleId可以像atlas [4]一样作为一个全局变量存在或保存在一个自定义单例中\n apkModuleId = strtol(argv[0], NULL, 16);\n }\n ... \n} \n\n```\n\n```\n//ResourceTypes.cpp 第二个改动点\n...\nDynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib)\n : mAssignedPackageId(packageId)\n , mAppAsLib(appAsLib)\n{\n memset(mLookupTable, 0, sizeof(mLookupTable));\n\n // Reserved package ids\n mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID;\n mLookupTable[apkModuleId] = apkModuleId;\n mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID;\n}\n...\nbool ResTable::stringToValue(Res_value* outValue, String16* outString,\n const char16_t* s, size_t len,\n bool preserveSpaces, bool coerceType,\n uint32_t attrID,\n const String16* defType,\n const String16* defPackage,\n Accessor* accessor,\n void* accessorCookie,\n uint32_t attrType,\n bool enforcePrivate) const\n{\n\n //只要在if (packageId != APP_PACKAGE_ID && packageId != SYS_PACKAGE_ID)的条件语句中加入自定义的packageid判断就行,如下所示\n if (packageId != apkModuleId && packageId != APP_PACKAGE_ID &&\n packageId != SYS_PACKAGE_ID) {\n ... \n }\n} \n\nstatus_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {\n //与stringToValue函数处理类,相应if语句加入以下逻辑\n if ((packageId == apkModuleId || packageId == APP_PACKAGE_ID) && !mAppAsLib) {\n ...\n }\n} \n```\n\n## aapt2方式\n\naapt2原生就支持自定义PP段,比如现在的Android App Bundle就用到了此功能,其参数定义如下:\n\n>--package-id package-id [Specifies the package ID to use for your app. The \n>package ID that you specify must be greater than or equal to 0x7f unless used in combination with --allow-reserved-package-id.]\n\n但是 在sdk 6,7,8的aapt2只支持 0x7f-0xff区域的package id值,在9之后就我们添加 --allow-reserved-package-id参数就可以在0x02-0x7f区域。\n\n# 简化书写引用宿主资源\n\n插件中引用宿主资源,我们只要把宿主ap_文件参与插件编译即可。但我们在书写资源引用时,与引用android系统资源类似(@packageName:resType/resName),必须带上宿主包名,如 @xxx.xx.xx:color/white。如果我们不重写appt做相应处理,那在IDEA里会容易爆红。\n解决方法:重写aapt,让业务插件不用书写包名过程,像引用plugin自身资源一样,直接找到资源。\n## aapt方式\n与修改packageid类似,我们加入自定义\n```\n//Main.cpp 第一个改动点\nint main(int argc, char* const argv[])\n{\n ...\n if(strcmp(cp, \"-host-package\") == 0) == 0){\n argc--;\n argv++;\n if (!argc) {\n fprintf(stderr, \"ERROR: No argument supplied for '--host-package' option\\n\");\n wantUsage = true;\n goto bail;\n }\n hostPackage=std::string(argv[0]);\n }\n ... \n} \n\n```\n\n```\n//ResourceTypes.cpp 第二个改动点\nbool ResTable::stringToValue(Res_value* outValue, String16* outString,\n const char16_t* s, size_t len,\n bool preserveSpaces, bool coerceType,\n uint32_t attrID,\n const String16* defType,\n const String16* defPackage,\n Accessor* accessor,\n void* accessorCookie,\n uint32_t attrType,\n bool enforcePrivate) const\n{\n\n ...\n if (*s == '#') {\n //末尾\n if (hostPackage.length() > 0 &&\n *defPackage != String16(hostPackage.c_str())) {\n String16 hostPackageName = String16(hostPackage.c_str());\n return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,\n &hostPackageName,\n accessor, accessorCookie, attrType, enforcePrivate);\n } else {\n if (accessor != NULL) {\n accessor->reportError(accessorCookie, \"No resource found that matches the given name\");\n }\n }\n return false;\n }\n\n\n if (*s == '?') {\n //末尾\n if (hostPackage.length() > 0 &&\n *defPackage != String16(hostPackage.c_str())) {\n String16 hostPackageName = String16(hostPackage.c_str());\n return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,\n &hostPackageName,\n accessor, accessorCookie, attrType, enforcePrivate);\n } else {\n if (accessor != NULL) {\n accessor->reportError(accessorCookie, \"No resource found that matches the given name\");\n }\n }\n return false;\n }\n\n} \n```\n\n## aapt2方式\n\n与aapt类似,ResourceTypes.cpp和aapt是同个文件,不过入口函数没在Main.cpp里,而是Link.cpp。\n```\n//Link.cpp\nint Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) {\n ...\n Flags flags =\n Flags()\n .OptionalFlag(\"--host-package\",\n \"Specific host packageName for plugin refer resources from host\\n\",\n &host_package)\n ...\n} \n```\n\n# 参考\n[1] AAPT2 USER GUIDE: [https://developer.android.com/studio/command-line/aapt2#link](https://developer.android.com/studio/command-line/aapt2#link) \n[2] aapt2 适配之资源id固定:[https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more](https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more) \n[3] Atlas 中文文档手册 :[https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md](https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md) \n[4] Atlas-aapt: [https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md](https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md) ","slug":"插件化之AAPT客户化","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n89000hcatefhjg1dh4","content":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>这篇文章其实是借花献佛,微店的aapt工作,绝大部分是同事<a href=\"https://fucknmb.com/about/\">区长</a>编码和验证,文章本意在于对插件化架构中aapt相关流程的总结。<br>在Android编译流程中,aapt主要处理资源相关工作。插件化架构中,需要客户化aapt,让其承担宿主导出符号表、宿主导入符号表、插件导入宿主ap_文件、 修改插件package id、简化书写插件引用宿主资源等额外功能。</p>\n<span id=\"more\"></span>\n<h1 id=\"宿主导出符号表\"><a href=\"#宿主导出符号表\" class=\"headerlink\" title=\"宿主导出符号表\"></a>宿主导出符号表</h1><p>宿主导出符号表主要用于:固定资源值;供插件调用宿主资源;aapt和aapt2实现方式不一样。</p>\n<h2 id=\"aapt方式\"><a href=\"#aapt方式\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>通过aapt link -P 参数实现。更多参数可通过(aapt –help)查看。</p>\n<blockquote>\n<p>-P specify where to output public resource definitions </p>\n</blockquote>\n<figure class=\"highlight groovy\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//DSL</span></span><br><span class=\"line\">android {</span><br><span class=\"line\"> aaptOptions {</span><br><span class=\"line\"> additionalParameters <span class=\"string\">"-P"</span>, <span class=\"string\">"${project.getBuildDir()}/public.xml"</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">//伪代码</span></span><br><span class=\"line\"> additionalParameters.add(<span class=\"string\">"-P"</span>)</span><br><span class=\"line\"> additionalParameters.add(redirectFile.getAbsolutePath())</span><br><span class=\"line\"> processAndroidResourceTask.doLast {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (redirectFile.exists()) {</span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>\\n"</span>)</span><br><span class=\"line\"> exportSymbolFile.write(<span class=\"string\">"<!-- AUTO-GENERATED FILE. DO NOT MODIFY -->\\n"</span>)</span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">"<resources>\\n"</span>)</span><br><span class=\"line\"> <span class=\"keyword\">def</span> publicXMLNodes = <span class=\"keyword\">new</span> XmlParser().parse(redirectFile)</span><br><span class=\"line\"> publicXMLNodes.each {</span><br><span class=\"line\"> <span class=\"comment\">//这里可以有选择导出,比如只导出style和attr资源</span></span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">" <public type=\\"${it.@type}\\" name=\\"${it.@name}\\" id=\\"${it.@id}\\" />\\n"</span>)</span><br><span class=\"line\"> }</span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">"</resources>"</span>)</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>导出文件类似于:</p>\n <figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><resources></span><br><span class=\"line\"> <public type="attr" name="barrierAllowsGoneWidgets" id="0x7f010004" /></span><br><span class=\"line\"> <public type="attr" name="barrierDirection" id="0x7f010005" /></span><br><span class=\"line\"></resources></span><br></pre></td></tr></table></figure>\n\n\n<h2 id=\"aapt2方式\"><a href=\"#aapt2方式\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>通过aapt2 link –emit-ids 参数实现。更多参数可查看<a href=\"https://developer.android.com/studio/command-line/aapt2#link\">aapt2</a>文档[1]。</p>\n<blockquote>\n<p>–emit-ids Emits a file at the given path with a list of names of resource<br>types and their ID mappings. It is suitable to use with –stable-ids.<br>可以产生资源id文件,可以适用于–stable-ids</p>\n</blockquote>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//DSL</span><br><span class=\"line\">android {</span><br><span class=\"line\"> aaptOptions {</span><br><span class=\"line\"> additionalParameters "--emit-ids", "${project.file('public.txt')}"</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\">//伪代码</span><br><span class=\"line\">additionalParameters.add("--emit-ids")</span><br><span class=\"line\">additionalParameters.add(redirectFile.getAbsolutePath())</span><br><span class=\"line\">processAndroidResourceTask.doLast {</span><br><span class=\"line\"> if (redirectFile.exists()) {</span><br><span class=\"line\"> Pattern filterPattern = Pattern.compile(".*?:(.*?)/.*?")</span><br><span class=\"line\"> List<String> sortedLines = new ArrayList<>()</span><br><span class=\"line\"> redirectFile.eachLine { String line -></span><br><span class=\"line\"> Matcher matcher = filterPattern.matcher(line)</span><br><span class=\"line\"> if (matcher.matches() && matcher.groupCount() == 1) {</span><br><span class=\"line\"> //这里可以有选择导出,比如只导出style和attr资源</span><br><span class=\"line\"> exportSymbolFile.append("${line}\\n")</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>导出文件类似于:</p>\n <figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">com.koudai.weidian.buyer:attr/barrierAllowsGoneWidgets = 0x7f010004</span><br><span class=\"line\">com.koudai.weidian.buyer:attr/barrierDirection = 0x7f010005</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"宿主导入符号表\"><a href=\"#宿主导入符号表\" class=\"headerlink\" title=\"宿主导入符号表\"></a>宿主导入符号表</h1><p>宿主导入符号表主要用于资源ID固定,在patch和插件化中常见。同样aapt和aapt2处理方式有所不同。</p>\n<h2 id=\"aapt方式-1\"><a href=\"#aapt方式-1\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>aapt与aapt2相比较为简单,我们只要在aapt link(对应 processResouceTask)前(比如 mergeResourceTask或 processManifestTask)把public.xml和ids.xml放到res目录中,参与后续的aapt.link编译就行。</p>\n<p>这里有个知识点:</p>\n<blockquote>\n<p>导入public.xml时,如果public.xml里有id<br>资源,但在values.xml里不存在,要额外新建一个xml文件,在文件里把id定义补上。</p>\n</blockquote>\n<h2 id=\"aapt2方式-1\"><a href=\"#aapt2方式-1\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>aapt2导入public.txt也较为简单,主要利用aapt2 –stable-ids 参数。</p>\n<blockquote>\n<p>–emit-ids path [Emits a file at the given path with a list of names of<br>resource types and their ID mappings. It is suitable to use with–stable-ids.]<br>–stable-ids outputfilename.ext [Consumes the file generated with –emit-ids<br>containing the list of names of resource types and their assigned IDs. This<br>option allows assigned IDs to remain stable even when you delete or add new<br>resources while linking.]</p>\n</blockquote>\n<p>但在插件化架构中,仅仅导入还不行,我们还要将导入的资源打上public flag标,才能供插件引用。打public flag标较为复杂,可参考团队同事写的<a href=\"https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more\">aapt2 适配之资源 id 固定</a>[2] ,其原理是:</p>\n<blockquote>\n<p>aapt2 在compile阶段就为资源打上public标,–stable-ids参数仅让资源参与link。</p>\n</blockquote>\n<h1 id=\"插件导入宿主ap\"><a href=\"#插件导入宿主ap\" class=\"headerlink\" title=\"插件导入宿主ap_\"></a>插件导入宿主ap_</h1><p>插件导入宿主ap_文件,是为了能有效资源共享,在我们的插件化体系中,还有就是解决宿主合并插件Manifest后,透明主题无法动态生效等问题。<br>导入ap_文件,aapt和aapt2实现一致,主要利用-I参数,让ap_文件参与aapt link执行过程,参数说明如下:</p>\n<blockquote>\n<p>-I add an existing package to base include set</p>\n</blockquote>\n<h1 id=\"修改插件资源packageid\"><a href=\"#修改插件资源packageid\" class=\"headerlink\" title=\"修改插件资源packageid\"></a>修改插件资源packageid</h1><p>修改pacakge id,主要是指修改资源的包名id,即0xPPTTEEEE(PackageId+TypeId+EntryId)中的PP段值。在Android体系中,非系统应用,资源PP段都是0x7f。在国内ROM里,资源ID的可分配区间为[0x21,0x7f)[3],主要原因在于:</p>\n<blockquote>\n<p>0x1x为系统保留 0x7f为主apk使用 0x20之前的发现miui里面内部已使用</p>\n</blockquote>\n<p>特别注意是,在联想ZUI ROM系统中,0x4f被占用。PP段固定业界实现有两种:修改二进制文件(arsc文件相关处理)和客户化aapt。二进制文件实现方式有滴滴的<a href=\"https://github.com/didi/VirtualAPK/wiki\">VirtualAPK</a>,<a href=\"https://github.com/wequick/Small\">Small</a>等开源实现。我们这里仅讲解aapt实现方式。</p>\n<h2 id=\"aapt方式-2\"><a href=\"#aapt方式-2\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>aapt修改packageid,需要我们修改aapt源码,业界实现方式大同小异,aapt各版本改动也可能不同。我们以sdk 9的版本为例,简述如何修改,大体思路如下:</p>\n<blockquote>\n<p>加入自定义参数-apk-module,在Main.cpp中获取其值,并在ResourceTypes.cpp中植入。</p>\n</blockquote>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//Main.cpp 第一个改动点</span><br><span class=\"line\">int main(int argc, char* const argv[])</span><br><span class=\"line\">{</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> if(strcmp(cp, "-apk-module") == 0){</span><br><span class=\"line\"> argc--;</span><br><span class=\"line\"> argv++;</span><br><span class=\"line\"> if (!argc) {</span><br><span class=\"line\"> fprintf(stderr, "ERROR: No argument supplied for '--apk-module' option\\n");</span><br><span class=\"line\"> wantUsage = true;</span><br><span class=\"line\"> goto bail;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> //取得 package id, apkModuleId可以像atlas [4]一样作为一个全局变量存在或保存在一个自定义单例中</span><br><span class=\"line\"> apkModuleId = strtol(argv[0], NULL, 16);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ... </span><br><span class=\"line\">} </span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//ResourceTypes.cpp 第二个改动点</span><br><span class=\"line\">...</span><br><span class=\"line\">DynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib)</span><br><span class=\"line\"> : mAssignedPackageId(packageId)</span><br><span class=\"line\"> , mAppAsLib(appAsLib)</span><br><span class=\"line\">{</span><br><span class=\"line\"> memset(mLookupTable, 0, sizeof(mLookupTable));</span><br><span class=\"line\"></span><br><span class=\"line\"> // Reserved package ids</span><br><span class=\"line\"> mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID;</span><br><span class=\"line\"> mLookupTable[apkModuleId] = apkModuleId;</span><br><span class=\"line\"> mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID;</span><br><span class=\"line\">}</span><br><span class=\"line\">...</span><br><span class=\"line\">bool ResTable::stringToValue(Res_value* outValue, String16* outString,</span><br><span class=\"line\"> const char16_t* s, size_t len,</span><br><span class=\"line\"> bool preserveSpaces, bool coerceType,</span><br><span class=\"line\"> uint32_t attrID,</span><br><span class=\"line\"> const String16* defType,</span><br><span class=\"line\"> const String16* defPackage,</span><br><span class=\"line\"> Accessor* accessor,</span><br><span class=\"line\"> void* accessorCookie,</span><br><span class=\"line\"> uint32_t attrType,</span><br><span class=\"line\"> bool enforcePrivate) const</span><br><span class=\"line\">{</span><br><span class=\"line\"></span><br><span class=\"line\"> //只要在if (packageId != APP_PACKAGE_ID && packageId != SYS_PACKAGE_ID)的条件语句中加入自定义的packageid判断就行,如下所示</span><br><span class=\"line\"> if (packageId != apkModuleId && packageId != APP_PACKAGE_ID &&</span><br><span class=\"line\"> packageId != SYS_PACKAGE_ID) {</span><br><span class=\"line\"> ... </span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br><span class=\"line\"></span><br><span class=\"line\">status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {</span><br><span class=\"line\"> //与stringToValue函数处理类,相应if语句加入以下逻辑</span><br><span class=\"line\"> if ((packageId == apkModuleId || packageId == APP_PACKAGE_ID) && !mAppAsLib) {</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<h2 id=\"aapt2方式-2\"><a href=\"#aapt2方式-2\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>aapt2原生就支持自定义PP段,比如现在的Android App Bundle就用到了此功能,其参数定义如下:</p>\n<blockquote>\n<p>–package-id package-id [Specifies the package ID to use for your app. The<br>package ID that you specify must be greater than or equal to 0x7f unless used in combination with –allow-reserved-package-id.]</p>\n</blockquote>\n<p>但是 在sdk 6,7,8的aapt2只支持 0x7f-0xff区域的package id值,在9之后就我们添加 –allow-reserved-package-id参数就可以在0x02-0x7f区域。</p>\n<h1 id=\"简化书写引用宿主资源\"><a href=\"#简化书写引用宿主资源\" class=\"headerlink\" title=\"简化书写引用宿主资源\"></a>简化书写引用宿主资源</h1><p>插件中引用宿主资源,我们只要把宿主ap_文件参与插件编译即可。但我们在书写资源引用时,与引用android系统资源类似(@packageName:resType/resName),必须带上宿主包名,如 @xxx.xx.xx:color/white。如果我们不重写appt做相应处理,那在IDEA里会容易爆红。<br>解决方法:重写aapt,让业务插件不用书写包名过程,像引用plugin自身资源一样,直接找到资源。</p>\n<h2 id=\"aapt方式-3\"><a href=\"#aapt方式-3\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>与修改packageid类似,我们加入自定义</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//Main.cpp 第一个改动点</span><br><span class=\"line\">int main(int argc, char* const argv[])</span><br><span class=\"line\">{</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> if(strcmp(cp, "-host-package") == 0) == 0){</span><br><span class=\"line\"> argc--;</span><br><span class=\"line\"> argv++;</span><br><span class=\"line\"> if (!argc) {</span><br><span class=\"line\"> fprintf(stderr, "ERROR: No argument supplied for '--host-package' option\\n");</span><br><span class=\"line\"> wantUsage = true;</span><br><span class=\"line\"> goto bail;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> hostPackage=std::string(argv[0]);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ... </span><br><span class=\"line\">} </span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//ResourceTypes.cpp 第二个改动点</span><br><span class=\"line\">bool ResTable::stringToValue(Res_value* outValue, String16* outString,</span><br><span class=\"line\"> const char16_t* s, size_t len,</span><br><span class=\"line\"> bool preserveSpaces, bool coerceType,</span><br><span class=\"line\"> uint32_t attrID,</span><br><span class=\"line\"> const String16* defType,</span><br><span class=\"line\"> const String16* defPackage,</span><br><span class=\"line\"> Accessor* accessor,</span><br><span class=\"line\"> void* accessorCookie,</span><br><span class=\"line\"> uint32_t attrType,</span><br><span class=\"line\"> bool enforcePrivate) const</span><br><span class=\"line\">{</span><br><span class=\"line\"></span><br><span class=\"line\"> ...</span><br><span class=\"line\"> if (*s == '#') {</span><br><span class=\"line\"> //末尾</span><br><span class=\"line\"> if (hostPackage.length() > 0 &&</span><br><span class=\"line\"> *defPackage != String16(hostPackage.c_str())) {</span><br><span class=\"line\"> String16 hostPackageName = String16(hostPackage.c_str());</span><br><span class=\"line\"> return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,</span><br><span class=\"line\"> &hostPackageName,</span><br><span class=\"line\"> accessor, accessorCookie, attrType, enforcePrivate);</span><br><span class=\"line\"> } else {</span><br><span class=\"line\"> if (accessor != NULL) {</span><br><span class=\"line\"> accessor->reportError(accessorCookie, "No resource found that matches the given name");</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return false;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\"> if (*s == '?') {</span><br><span class=\"line\"> //末尾</span><br><span class=\"line\"> if (hostPackage.length() > 0 &&</span><br><span class=\"line\"> *defPackage != String16(hostPackage.c_str())) {</span><br><span class=\"line\"> String16 hostPackageName = String16(hostPackage.c_str());</span><br><span class=\"line\"> return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,</span><br><span class=\"line\"> &hostPackageName,</span><br><span class=\"line\"> accessor, accessorCookie, attrType, enforcePrivate);</span><br><span class=\"line\"> } else {</span><br><span class=\"line\"> if (accessor != NULL) {</span><br><span class=\"line\"> accessor->reportError(accessorCookie, "No resource found that matches the given name");</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return false;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<h2 id=\"aapt2方式-3\"><a href=\"#aapt2方式-3\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>与aapt类似,ResourceTypes.cpp和aapt是同个文件,不过入口函数没在Main.cpp里,而是Link.cpp。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//Link.cpp</span><br><span class=\"line\">int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) {</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> Flags flags =</span><br><span class=\"line\"> Flags()</span><br><span class=\"line\"> .OptionalFlag("--host-package",</span><br><span class=\"line\"> "Specific host packageName for plugin refer resources from host\\n",</span><br><span class=\"line\"> &host_package)</span><br><span class=\"line\"> ...</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>[1] AAPT2 USER GUIDE: <a href=\"https://developer.android.com/studio/command-line/aapt2#link\">https://developer.android.com/studio/command-line/aapt2#link</a><br>[2] aapt2 适配之资源id固定:<a href=\"https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more\">https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more</a><br>[3] Atlas 中文文档手册 :<a href=\"https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md\">https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md</a><br>[4] Atlas-aapt: <a href=\"https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md\">https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md</a> </p>\n","excerpt":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>这篇文章其实是借花献佛,微店的aapt工作,绝大部分是同事<a href=\"https://fucknmb.com/about/\">区长</a>编码和验证,文章本意在于对插件化架构中aapt相关流程的总结。<br>在Android编译流程中,aapt主要处理资源相关工作。插件化架构中,需要客户化aapt,让其承担宿主导出符号表、宿主导入符号表、插件导入宿主ap_文件、 修改插件package id、简化书写插件引用宿主资源等额外功能。</p>","more":"<h1 id=\"宿主导出符号表\"><a href=\"#宿主导出符号表\" class=\"headerlink\" title=\"宿主导出符号表\"></a>宿主导出符号表</h1><p>宿主导出符号表主要用于:固定资源值;供插件调用宿主资源;aapt和aapt2实现方式不一样。</p>\n<h2 id=\"aapt方式\"><a href=\"#aapt方式\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>通过aapt link -P 参数实现。更多参数可通过(aapt –help)查看。</p>\n<blockquote>\n<p>-P specify where to output public resource definitions </p>\n</blockquote>\n<figure class=\"highlight groovy\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">//DSL</span></span><br><span class=\"line\">android {</span><br><span class=\"line\"> aaptOptions {</span><br><span class=\"line\"> additionalParameters <span class=\"string\">"-P"</span>, <span class=\"string\">"${project.getBuildDir()}/public.xml"</span></span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">//伪代码</span></span><br><span class=\"line\"> additionalParameters.add(<span class=\"string\">"-P"</span>)</span><br><span class=\"line\"> additionalParameters.add(redirectFile.getAbsolutePath())</span><br><span class=\"line\"> processAndroidResourceTask.doLast {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (redirectFile.exists()) {</span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">"<?xml version=\\"1.0\\" encoding=\\"utf-8\\"?>\\n"</span>)</span><br><span class=\"line\"> exportSymbolFile.write(<span class=\"string\">"<!-- AUTO-GENERATED FILE. DO NOT MODIFY -->\\n"</span>)</span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">"<resources>\\n"</span>)</span><br><span class=\"line\"> <span class=\"keyword\">def</span> publicXMLNodes = <span class=\"keyword\">new</span> XmlParser().parse(redirectFile)</span><br><span class=\"line\"> publicXMLNodes.each {</span><br><span class=\"line\"> <span class=\"comment\">//这里可以有选择导出,比如只导出style和attr资源</span></span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">" <public type=\\"${it.@type}\\" name=\\"${it.@name}\\" id=\\"${it.@id}\\" />\\n"</span>)</span><br><span class=\"line\"> }</span><br><span class=\"line\"> exportSymbolFile.append(<span class=\"string\">"</resources>"</span>)</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>导出文件类似于:</p>\n <figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><resources></span><br><span class=\"line\"> <public type="attr" name="barrierAllowsGoneWidgets" id="0x7f010004" /></span><br><span class=\"line\"> <public type="attr" name="barrierDirection" id="0x7f010005" /></span><br><span class=\"line\"></resources></span><br></pre></td></tr></table></figure>\n\n\n<h2 id=\"aapt2方式\"><a href=\"#aapt2方式\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>通过aapt2 link –emit-ids 参数实现。更多参数可查看<a href=\"https://developer.android.com/studio/command-line/aapt2#link\">aapt2</a>文档[1]。</p>\n<blockquote>\n<p>–emit-ids Emits a file at the given path with a list of names of resource<br>types and their ID mappings. It is suitable to use with –stable-ids.<br>可以产生资源id文件,可以适用于–stable-ids</p>\n</blockquote>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//DSL</span><br><span class=\"line\">android {</span><br><span class=\"line\"> aaptOptions {</span><br><span class=\"line\"> additionalParameters "--emit-ids", "${project.file('public.txt')}"</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\">//伪代码</span><br><span class=\"line\">additionalParameters.add("--emit-ids")</span><br><span class=\"line\">additionalParameters.add(redirectFile.getAbsolutePath())</span><br><span class=\"line\">processAndroidResourceTask.doLast {</span><br><span class=\"line\"> if (redirectFile.exists()) {</span><br><span class=\"line\"> Pattern filterPattern = Pattern.compile(".*?:(.*?)/.*?")</span><br><span class=\"line\"> List<String> sortedLines = new ArrayList<>()</span><br><span class=\"line\"> redirectFile.eachLine { String line -></span><br><span class=\"line\"> Matcher matcher = filterPattern.matcher(line)</span><br><span class=\"line\"> if (matcher.matches() && matcher.groupCount() == 1) {</span><br><span class=\"line\"> //这里可以有选择导出,比如只导出style和attr资源</span><br><span class=\"line\"> exportSymbolFile.append("${line}\\n")</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>导出文件类似于:</p>\n <figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">com.koudai.weidian.buyer:attr/barrierAllowsGoneWidgets = 0x7f010004</span><br><span class=\"line\">com.koudai.weidian.buyer:attr/barrierDirection = 0x7f010005</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"宿主导入符号表\"><a href=\"#宿主导入符号表\" class=\"headerlink\" title=\"宿主导入符号表\"></a>宿主导入符号表</h1><p>宿主导入符号表主要用于资源ID固定,在patch和插件化中常见。同样aapt和aapt2处理方式有所不同。</p>\n<h2 id=\"aapt方式-1\"><a href=\"#aapt方式-1\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>aapt与aapt2相比较为简单,我们只要在aapt link(对应 processResouceTask)前(比如 mergeResourceTask或 processManifestTask)把public.xml和ids.xml放到res目录中,参与后续的aapt.link编译就行。</p>\n<p>这里有个知识点:</p>\n<blockquote>\n<p>导入public.xml时,如果public.xml里有id<br>资源,但在values.xml里不存在,要额外新建一个xml文件,在文件里把id定义补上。</p>\n</blockquote>\n<h2 id=\"aapt2方式-1\"><a href=\"#aapt2方式-1\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>aapt2导入public.txt也较为简单,主要利用aapt2 –stable-ids 参数。</p>\n<blockquote>\n<p>–emit-ids path [Emits a file at the given path with a list of names of<br>resource types and their ID mappings. It is suitable to use with–stable-ids.]<br>–stable-ids outputfilename.ext [Consumes the file generated with –emit-ids<br>containing the list of names of resource types and their assigned IDs. This<br>option allows assigned IDs to remain stable even when you delete or add new<br>resources while linking.]</p>\n</blockquote>\n<p>但在插件化架构中,仅仅导入还不行,我们还要将导入的资源打上public flag标,才能供插件引用。打public flag标较为复杂,可参考团队同事写的<a href=\"https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more\">aapt2 适配之资源 id 固定</a>[2] ,其原理是:</p>\n<blockquote>\n<p>aapt2 在compile阶段就为资源打上public标,–stable-ids参数仅让资源参与link。</p>\n</blockquote>\n<h1 id=\"插件导入宿主ap\"><a href=\"#插件导入宿主ap\" class=\"headerlink\" title=\"插件导入宿主ap_\"></a>插件导入宿主ap_</h1><p>插件导入宿主ap_文件,是为了能有效资源共享,在我们的插件化体系中,还有就是解决宿主合并插件Manifest后,透明主题无法动态生效等问题。<br>导入ap_文件,aapt和aapt2实现一致,主要利用-I参数,让ap_文件参与aapt link执行过程,参数说明如下:</p>\n<blockquote>\n<p>-I add an existing package to base include set</p>\n</blockquote>\n<h1 id=\"修改插件资源packageid\"><a href=\"#修改插件资源packageid\" class=\"headerlink\" title=\"修改插件资源packageid\"></a>修改插件资源packageid</h1><p>修改pacakge id,主要是指修改资源的包名id,即0xPPTTEEEE(PackageId+TypeId+EntryId)中的PP段值。在Android体系中,非系统应用,资源PP段都是0x7f。在国内ROM里,资源ID的可分配区间为[0x21,0x7f)[3],主要原因在于:</p>\n<blockquote>\n<p>0x1x为系统保留 0x7f为主apk使用 0x20之前的发现miui里面内部已使用</p>\n</blockquote>\n<p>特别注意是,在联想ZUI ROM系统中,0x4f被占用。PP段固定业界实现有两种:修改二进制文件(arsc文件相关处理)和客户化aapt。二进制文件实现方式有滴滴的<a href=\"https://github.com/didi/VirtualAPK/wiki\">VirtualAPK</a>,<a href=\"https://github.com/wequick/Small\">Small</a>等开源实现。我们这里仅讲解aapt实现方式。</p>\n<h2 id=\"aapt方式-2\"><a href=\"#aapt方式-2\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>aapt修改packageid,需要我们修改aapt源码,业界实现方式大同小异,aapt各版本改动也可能不同。我们以sdk 9的版本为例,简述如何修改,大体思路如下:</p>\n<blockquote>\n<p>加入自定义参数-apk-module,在Main.cpp中获取其值,并在ResourceTypes.cpp中植入。</p>\n</blockquote>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//Main.cpp 第一个改动点</span><br><span class=\"line\">int main(int argc, char* const argv[])</span><br><span class=\"line\">{</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> if(strcmp(cp, "-apk-module") == 0){</span><br><span class=\"line\"> argc--;</span><br><span class=\"line\"> argv++;</span><br><span class=\"line\"> if (!argc) {</span><br><span class=\"line\"> fprintf(stderr, "ERROR: No argument supplied for '--apk-module' option\\n");</span><br><span class=\"line\"> wantUsage = true;</span><br><span class=\"line\"> goto bail;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> //取得 package id, apkModuleId可以像atlas [4]一样作为一个全局变量存在或保存在一个自定义单例中</span><br><span class=\"line\"> apkModuleId = strtol(argv[0], NULL, 16);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ... </span><br><span class=\"line\">} </span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//ResourceTypes.cpp 第二个改动点</span><br><span class=\"line\">...</span><br><span class=\"line\">DynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib)</span><br><span class=\"line\"> : mAssignedPackageId(packageId)</span><br><span class=\"line\"> , mAppAsLib(appAsLib)</span><br><span class=\"line\">{</span><br><span class=\"line\"> memset(mLookupTable, 0, sizeof(mLookupTable));</span><br><span class=\"line\"></span><br><span class=\"line\"> // Reserved package ids</span><br><span class=\"line\"> mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID;</span><br><span class=\"line\"> mLookupTable[apkModuleId] = apkModuleId;</span><br><span class=\"line\"> mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID;</span><br><span class=\"line\">}</span><br><span class=\"line\">...</span><br><span class=\"line\">bool ResTable::stringToValue(Res_value* outValue, String16* outString,</span><br><span class=\"line\"> const char16_t* s, size_t len,</span><br><span class=\"line\"> bool preserveSpaces, bool coerceType,</span><br><span class=\"line\"> uint32_t attrID,</span><br><span class=\"line\"> const String16* defType,</span><br><span class=\"line\"> const String16* defPackage,</span><br><span class=\"line\"> Accessor* accessor,</span><br><span class=\"line\"> void* accessorCookie,</span><br><span class=\"line\"> uint32_t attrType,</span><br><span class=\"line\"> bool enforcePrivate) const</span><br><span class=\"line\">{</span><br><span class=\"line\"></span><br><span class=\"line\"> //只要在if (packageId != APP_PACKAGE_ID && packageId != SYS_PACKAGE_ID)的条件语句中加入自定义的packageid判断就行,如下所示</span><br><span class=\"line\"> if (packageId != apkModuleId && packageId != APP_PACKAGE_ID &&</span><br><span class=\"line\"> packageId != SYS_PACKAGE_ID) {</span><br><span class=\"line\"> ... </span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br><span class=\"line\"></span><br><span class=\"line\">status_t DynamicRefTable::lookupResourceId(uint32_t* resId) const {</span><br><span class=\"line\"> //与stringToValue函数处理类,相应if语句加入以下逻辑</span><br><span class=\"line\"> if ((packageId == apkModuleId || packageId == APP_PACKAGE_ID) && !mAppAsLib) {</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<h2 id=\"aapt2方式-2\"><a href=\"#aapt2方式-2\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>aapt2原生就支持自定义PP段,比如现在的Android App Bundle就用到了此功能,其参数定义如下:</p>\n<blockquote>\n<p>–package-id package-id [Specifies the package ID to use for your app. The<br>package ID that you specify must be greater than or equal to 0x7f unless used in combination with –allow-reserved-package-id.]</p>\n</blockquote>\n<p>但是 在sdk 6,7,8的aapt2只支持 0x7f-0xff区域的package id值,在9之后就我们添加 –allow-reserved-package-id参数就可以在0x02-0x7f区域。</p>\n<h1 id=\"简化书写引用宿主资源\"><a href=\"#简化书写引用宿主资源\" class=\"headerlink\" title=\"简化书写引用宿主资源\"></a>简化书写引用宿主资源</h1><p>插件中引用宿主资源,我们只要把宿主ap_文件参与插件编译即可。但我们在书写资源引用时,与引用android系统资源类似(@packageName:resType/resName),必须带上宿主包名,如 @xxx.xx.xx:color/white。如果我们不重写appt做相应处理,那在IDEA里会容易爆红。<br>解决方法:重写aapt,让业务插件不用书写包名过程,像引用plugin自身资源一样,直接找到资源。</p>\n<h2 id=\"aapt方式-3\"><a href=\"#aapt方式-3\" class=\"headerlink\" title=\"aapt方式\"></a>aapt方式</h2><p>与修改packageid类似,我们加入自定义</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//Main.cpp 第一个改动点</span><br><span class=\"line\">int main(int argc, char* const argv[])</span><br><span class=\"line\">{</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> if(strcmp(cp, "-host-package") == 0) == 0){</span><br><span class=\"line\"> argc--;</span><br><span class=\"line\"> argv++;</span><br><span class=\"line\"> if (!argc) {</span><br><span class=\"line\"> fprintf(stderr, "ERROR: No argument supplied for '--host-package' option\\n");</span><br><span class=\"line\"> wantUsage = true;</span><br><span class=\"line\"> goto bail;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> hostPackage=std::string(argv[0]);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> ... </span><br><span class=\"line\">} </span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//ResourceTypes.cpp 第二个改动点</span><br><span class=\"line\">bool ResTable::stringToValue(Res_value* outValue, String16* outString,</span><br><span class=\"line\"> const char16_t* s, size_t len,</span><br><span class=\"line\"> bool preserveSpaces, bool coerceType,</span><br><span class=\"line\"> uint32_t attrID,</span><br><span class=\"line\"> const String16* defType,</span><br><span class=\"line\"> const String16* defPackage,</span><br><span class=\"line\"> Accessor* accessor,</span><br><span class=\"line\"> void* accessorCookie,</span><br><span class=\"line\"> uint32_t attrType,</span><br><span class=\"line\"> bool enforcePrivate) const</span><br><span class=\"line\">{</span><br><span class=\"line\"></span><br><span class=\"line\"> ...</span><br><span class=\"line\"> if (*s == '#') {</span><br><span class=\"line\"> //末尾</span><br><span class=\"line\"> if (hostPackage.length() > 0 &&</span><br><span class=\"line\"> *defPackage != String16(hostPackage.c_str())) {</span><br><span class=\"line\"> String16 hostPackageName = String16(hostPackage.c_str());</span><br><span class=\"line\"> return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,</span><br><span class=\"line\"> &hostPackageName,</span><br><span class=\"line\"> accessor, accessorCookie, attrType, enforcePrivate);</span><br><span class=\"line\"> } else {</span><br><span class=\"line\"> if (accessor != NULL) {</span><br><span class=\"line\"> accessor->reportError(accessorCookie, "No resource found that matches the given name");</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return false;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\"> if (*s == '?') {</span><br><span class=\"line\"> //末尾</span><br><span class=\"line\"> if (hostPackage.length() > 0 &&</span><br><span class=\"line\"> *defPackage != String16(hostPackage.c_str())) {</span><br><span class=\"line\"> String16 hostPackageName = String16(hostPackage.c_str());</span><br><span class=\"line\"> return stringToValue(outValue, outString, s, len, preserveSpaces, coerceType, attrID, defType,</span><br><span class=\"line\"> &hostPackageName,</span><br><span class=\"line\"> accessor, accessorCookie, attrType, enforcePrivate);</span><br><span class=\"line\"> } else {</span><br><span class=\"line\"> if (accessor != NULL) {</span><br><span class=\"line\"> accessor->reportError(accessorCookie, "No resource found that matches the given name");</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return false;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<h2 id=\"aapt2方式-3\"><a href=\"#aapt2方式-3\" class=\"headerlink\" title=\"aapt2方式\"></a>aapt2方式</h2><p>与aapt类似,ResourceTypes.cpp和aapt是同个文件,不过入口函数没在Main.cpp里,而是Link.cpp。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//Link.cpp</span><br><span class=\"line\">int Link(const std::vector<StringPiece>& args, IDiagnostics* diagnostics) {</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> Flags flags =</span><br><span class=\"line\"> Flags()</span><br><span class=\"line\"> .OptionalFlag("--host-package",</span><br><span class=\"line\"> "Specific host packageName for plugin refer resources from host\\n",</span><br><span class=\"line\"> &host_package)</span><br><span class=\"line\"> ...</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>[1] AAPT2 USER GUIDE: <a href=\"https://developer.android.com/studio/command-line/aapt2#link\">https://developer.android.com/studio/command-line/aapt2#link</a><br>[2] aapt2 适配之资源id固定:<a href=\"https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more\">https://fucknmb.com/2017/11/15/aapt2%E9%80%82%E9%85%8D%E4%B9%8B%E8%B5%84%E6%BA%90id%E5%9B%BA%E5%AE%9A/#more</a><br>[3] Atlas 中文文档手册 :<a href=\"https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md\">https://www.bookstack.cn/read/atlas-zh/atlas-docs-guide-for-use-guide_for_build.md</a><br>[4] Atlas-aapt: <a href=\"https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md\">https://github.com/alibaba/atlas/blob/master/atlas-aapt/README.zh-cn.md</a> </p>"},{"title":"插件化之启动优化实践","date":"2019-06-29T14:40:33.000Z","_content":"# 1、引言\n插件化是一把双刃剑,引入插件化实现后,每个APP都会面临插件框架带来的启动性能问题。性能问题不局限于以下所列项:\n1. ART Runtime上首次启动,dex2aot耗时问题; \n2. 内致的静态插件和部分SDK,启动时作了大量的初始化操作,表现为Splash页停留5s+以上; \n\n微店也不例外,在插件化运行初期,微店APP仅静态插件加载时长超5s的每日量达2w+,另个一个店长版APP更不乐观。针对启动时长,我们尝试做了以下优化。\n<!-- more -->\n# 2、启动优化\n优化前,微店APP启动流程可简化成下图所示。APP在Applicaction的attachBaseContext,onCreate以及SplashActivity的onStart,onWindowFocusChanged函数中实现了插件的加载和SDK的初始化。 \n1. application attachBaseContext阶段:Muitidex处理后,在异步线程,插件框架收集所有静态插件信息(插件文件,插件组件解析)并进行loadDex等操作; \n2. application onCreate阶段:执行必要的插件以及核心SDK初始化代码,以保证后续功能调用成功,这里的核心SDK包括诸如网络,Crash上报库,埋点库等; \n3. activity onStart阶段:这里再细化为主线程和异步线程,执行部分SDK的初始化,比如Fresco;\n4. activity onWindowFocusChanged阶段:这一阶段执行不太紧急的SDK初始化,比如IM长连接,第三方统计库等。 \n \n\n在这一阶段,已经做到了SDK分层加载,但还是不太理想,主要表现在:\n\n1. application attachBaseContext阶段,所有插件都主动加载,并未按需加载或懒加载,比如App里的足迹插件,其实可以做为懒加载插件,在启动足迹页时,进行插件加载; \n2. application onCreate阶段,大量的SDK初始化,直接加长了启动时长,直接表现为用户看到的splash图其实是window背景,splash页一直在loading中;\n3. 在Splash Activity周期函数中执行了SDK初始化代码,这里在绝大数场景下是没问题。但在自动化测试过程,可能不会先跳splash页,会引起由于SDK没初始化,导致异常表现。\n\n在收到华为市场启动时长过长警告后(华为要求电商APP启动时长不能超2s),决定优化启动流程。\n# 3、优化设计\n针对上章节问题,对流程重构,设计了以下流程。\n \n插件分为懒加载插件和非懒加载插件,启动时仅对非懒加载插件进行加载,懒加载插件仅做校验等工作。 \n在Application onCreate阶段,不再在主线程执行SDK初始化,在工作线程执行完核心SDK始化后,启动了多个其他工作线程对其余SDK进行操作; \n在首个Activity onResume中对必须在主线程执行的SDK进行操作;\n# 4、优化实现\n代码实现其实做了很多工作,非常优秀的团队小伙伴实现了包括ART上首次启动禁用dex2aot优化[1],应用秒开优化[2]。一些核心实现简述如下:\n\n1. ART首次启动优化,主要思路hook runtime->image\\_dex2oat\\_enabled_实现动态禁用dex2aot,由于Android版本多,rom包碎片化,这里涉及到大量的适配工作,主要在于runtime的偏移量矫正; \n2. 在启动后,会在工作进程对所有插件进行dex2aot操作[3];\n3. APP hook instrumentation, 在第一个Activity实例化时,如果SDK init工作还未完成,先启动一个HookedActivity,等所有工作完成后,再启动业务Splash页。\n\n如果是拦截启动了hookedActivity,一定要在hookedInstrumentation.callActivityOnCreate方法重置theme为hookActivity自身主题,否则可能会抛出异常,原因我们可以看[ActiviyThread.performLaunchActivity](http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#2808)的实现:\n\n```java\nprivate Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {\n.....\n //r是ActivityRecord,r.activityInfo是从目标activity组件信息里读取的\n int theme = r.activityInfo.getThemeResource();\n if (theme != 0) {\n activity.setTheme(theme);\n }\n activity.mCalled = false;\n if (r.isPersistable()) {\n mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);\n } else {\n mInstrumentation.callActivityOnCreate(activity, r.state);\n }\n \n..... \n}\n```\n经过一系列的优化后,我们拿到了比较理想的数据。从1w的采样数据我们发现,绝大多数启动在2s以内,有一定的量分布在10s以上。10s以上认定为不太正常的统计数据,通过onWindowFocusChanged方式无法适配Push唤醒,黑屏等场景。下一步的想法是只统计launcher程序启动APP时长,忽略其他唤醒方式,异常数据可能会大量减少。\n \n\n# 参考\n[1] art dex2oat 加载加速浅析:[https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/](https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/) \n[2] Android端应用秒开优化体验:[http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/](http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/) \n[3] atlas:[https://github.com/alibaba/atlas](https://github.com/alibaba/atlas) \n","source":"_posts/插件化之启动优化实践.md","raw":"---\ntitle: 插件化之启动优化实践\ndate: 2019-06-29 22:40:33\ncategories: \n\t- Android\ntags: \n\t- 插件化\n---\n# 1、引言\n插件化是一把双刃剑,引入插件化实现后,每个APP都会面临插件框架带来的启动性能问题。性能问题不局限于以下所列项:\n1. ART Runtime上首次启动,dex2aot耗时问题; \n2. 内致的静态插件和部分SDK,启动时作了大量的初始化操作,表现为Splash页停留5s+以上; \n\n微店也不例外,在插件化运行初期,微店APP仅静态插件加载时长超5s的每日量达2w+,另个一个店长版APP更不乐观。针对启动时长,我们尝试做了以下优化。\n<!-- more -->\n# 2、启动优化\n优化前,微店APP启动流程可简化成下图所示。APP在Applicaction的attachBaseContext,onCreate以及SplashActivity的onStart,onWindowFocusChanged函数中实现了插件的加载和SDK的初始化。 \n1. application attachBaseContext阶段:Muitidex处理后,在异步线程,插件框架收集所有静态插件信息(插件文件,插件组件解析)并进行loadDex等操作; \n2. application onCreate阶段:执行必要的插件以及核心SDK初始化代码,以保证后续功能调用成功,这里的核心SDK包括诸如网络,Crash上报库,埋点库等; \n3. activity onStart阶段:这里再细化为主线程和异步线程,执行部分SDK的初始化,比如Fresco;\n4. activity onWindowFocusChanged阶段:这一阶段执行不太紧急的SDK初始化,比如IM长连接,第三方统计库等。 \n \n\n在这一阶段,已经做到了SDK分层加载,但还是不太理想,主要表现在:\n\n1. application attachBaseContext阶段,所有插件都主动加载,并未按需加载或懒加载,比如App里的足迹插件,其实可以做为懒加载插件,在启动足迹页时,进行插件加载; \n2. application onCreate阶段,大量的SDK初始化,直接加长了启动时长,直接表现为用户看到的splash图其实是window背景,splash页一直在loading中;\n3. 在Splash Activity周期函数中执行了SDK初始化代码,这里在绝大数场景下是没问题。但在自动化测试过程,可能不会先跳splash页,会引起由于SDK没初始化,导致异常表现。\n\n在收到华为市场启动时长过长警告后(华为要求电商APP启动时长不能超2s),决定优化启动流程。\n# 3、优化设计\n针对上章节问题,对流程重构,设计了以下流程。\n \n插件分为懒加载插件和非懒加载插件,启动时仅对非懒加载插件进行加载,懒加载插件仅做校验等工作。 \n在Application onCreate阶段,不再在主线程执行SDK初始化,在工作线程执行完核心SDK始化后,启动了多个其他工作线程对其余SDK进行操作; \n在首个Activity onResume中对必须在主线程执行的SDK进行操作;\n# 4、优化实现\n代码实现其实做了很多工作,非常优秀的团队小伙伴实现了包括ART上首次启动禁用dex2aot优化[1],应用秒开优化[2]。一些核心实现简述如下:\n\n1. ART首次启动优化,主要思路hook runtime->image\\_dex2oat\\_enabled_实现动态禁用dex2aot,由于Android版本多,rom包碎片化,这里涉及到大量的适配工作,主要在于runtime的偏移量矫正; \n2. 在启动后,会在工作进程对所有插件进行dex2aot操作[3];\n3. APP hook instrumentation, 在第一个Activity实例化时,如果SDK init工作还未完成,先启动一个HookedActivity,等所有工作完成后,再启动业务Splash页。\n\n如果是拦截启动了hookedActivity,一定要在hookedInstrumentation.callActivityOnCreate方法重置theme为hookActivity自身主题,否则可能会抛出异常,原因我们可以看[ActiviyThread.performLaunchActivity](http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#2808)的实现:\n\n```java\nprivate Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {\n.....\n //r是ActivityRecord,r.activityInfo是从目标activity组件信息里读取的\n int theme = r.activityInfo.getThemeResource();\n if (theme != 0) {\n activity.setTheme(theme);\n }\n activity.mCalled = false;\n if (r.isPersistable()) {\n mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);\n } else {\n mInstrumentation.callActivityOnCreate(activity, r.state);\n }\n \n..... \n}\n```\n经过一系列的优化后,我们拿到了比较理想的数据。从1w的采样数据我们发现,绝大多数启动在2s以内,有一定的量分布在10s以上。10s以上认定为不太正常的统计数据,通过onWindowFocusChanged方式无法适配Push唤醒,黑屏等场景。下一步的想法是只统计launcher程序启动APP时长,忽略其他唤醒方式,异常数据可能会大量减少。\n \n\n# 参考\n[1] art dex2oat 加载加速浅析:[https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/](https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/) \n[2] Android端应用秒开优化体验:[http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/](http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/) \n[3] atlas:[https://github.com/alibaba/atlas](https://github.com/alibaba/atlas) \n","slug":"插件化之启动优化实践","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n8a000kcate93gt3c54","content":"<h1 id=\"1、引言\"><a href=\"#1、引言\" class=\"headerlink\" title=\"1、引言\"></a>1、引言</h1><p>插件化是一把双刃剑,引入插件化实现后,每个APP都会面临插件框架带来的启动性能问题。性能问题不局限于以下所列项:</p>\n<ol>\n<li>ART Runtime上首次启动,dex2aot耗时问题; </li>\n<li>内致的静态插件和部分SDK,启动时作了大量的初始化操作,表现为Splash页停留5s+以上;</li>\n</ol>\n<p>微店也不例外,在插件化运行初期,微店APP仅静态插件加载时长超5s的每日量达2w+,另个一个店长版APP更不乐观。针对启动时长,我们尝试做了以下优化。</p>\n<span id=\"more\"></span>\n<h1 id=\"2、启动优化\"><a href=\"#2、启动优化\" class=\"headerlink\" title=\"2、启动优化\"></a>2、启动优化</h1><p>优化前,微店APP启动流程可简化成下图所示。APP在Applicaction的attachBaseContext,onCreate以及SplashActivity的onStart,onWindowFocusChanged函数中实现了插件的加载和SDK的初始化。 </p>\n<ol>\n<li>application attachBaseContext阶段:Muitidex处理后,在异步线程,插件框架收集所有静态插件信息(插件文件,插件组件解析)并进行loadDex等操作; </li>\n<li>application onCreate阶段:执行必要的插件以及核心SDK初始化代码,以保证后续功能调用成功,这里的核心SDK包括诸如网络,Crash上报库,埋点库等; </li>\n<li>activity onStart阶段:这里再细化为主线程和异步线程,执行部分SDK的初始化,比如Fresco;</li>\n<li>activity onWindowFocusChanged阶段:这一阶段执行不太紧急的SDK初始化,比如IM长连接,第三方统计库等。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/appstart.png\"></li>\n</ol>\n<p>在这一阶段,已经做到了SDK分层加载,但还是不太理想,主要表现在:</p>\n<ol>\n<li>application attachBaseContext阶段,所有插件都主动加载,并未按需加载或懒加载,比如App里的足迹插件,其实可以做为懒加载插件,在启动足迹页时,进行插件加载; </li>\n<li>application onCreate阶段,大量的SDK初始化,直接加长了启动时长,直接表现为用户看到的splash图其实是window背景,splash页一直在loading中;</li>\n<li>在Splash Activity周期函数中执行了SDK初始化代码,这里在绝大数场景下是没问题。但在自动化测试过程,可能不会先跳splash页,会引起由于SDK没初始化,导致异常表现。</li>\n</ol>\n<p>在收到华为市场启动时长过长警告后(华为要求电商APP启动时长不能超2s),决定优化启动流程。</p>\n<h1 id=\"3、优化设计\"><a href=\"#3、优化设计\" class=\"headerlink\" title=\"3、优化设计\"></a>3、优化设计</h1><p>针对上章节问题,对流程重构,设计了以下流程。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/appstart2.png\"><br>插件分为懒加载插件和非懒加载插件,启动时仅对非懒加载插件进行加载,懒加载插件仅做校验等工作。<br>在Application onCreate阶段,不再在主线程执行SDK初始化,在工作线程执行完核心SDK始化后,启动了多个其他工作线程对其余SDK进行操作;<br>在首个Activity onResume中对必须在主线程执行的SDK进行操作;</p>\n<h1 id=\"4、优化实现\"><a href=\"#4、优化实现\" class=\"headerlink\" title=\"4、优化实现\"></a>4、优化实现</h1><p>代码实现其实做了很多工作,非常优秀的团队小伙伴实现了包括ART上首次启动禁用dex2aot优化[1],应用秒开优化[2]。一些核心实现简述如下:</p>\n<ol>\n<li>ART首次启动优化,主要思路hook runtime->image_dex2oat_enabled_实现动态禁用dex2aot,由于Android版本多,rom包碎片化,这里涉及到大量的适配工作,主要在于runtime的偏移量矫正; </li>\n<li>在启动后,会在工作进程对所有插件进行dex2aot操作[3];</li>\n<li>APP hook instrumentation, 在第一个Activity实例化时,如果SDK init工作还未完成,先启动一个HookedActivity,等所有工作完成后,再启动业务Splash页。</li>\n</ol>\n<p>如果是拦截启动了hookedActivity,一定要在hookedInstrumentation.callActivityOnCreate方法重置theme为hookActivity自身主题,否则可能会抛出异常,原因我们可以看<a href=\"http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#2808\">ActiviyThread.performLaunchActivity</a>的实现:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">private</span> Activity <span class=\"title function_\">performLaunchActivity</span><span class=\"params\">(ActivityClientRecord r, Intent customIntent)</span> {</span><br><span class=\"line\">.....</span><br><span class=\"line\"> <span class=\"comment\">//r是ActivityRecord,r.activityInfo是从目标activity组件信息里读取的</span></span><br><span class=\"line\"> <span class=\"type\">int</span> <span class=\"variable\">theme</span> <span class=\"operator\">=</span> r.activityInfo.getThemeResource();</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (theme != <span class=\"number\">0</span>) {</span><br><span class=\"line\"> activity.setTheme(theme);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> activity.mCalled = <span class=\"literal\">false</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (r.isPersistable()) {</span><br><span class=\"line\"> mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> mInstrumentation.callActivityOnCreate(activity, r.state);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> </span><br><span class=\"line\">..... </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>经过一系列的优化后,我们拿到了比较理想的数据。从1w的采样数据我们发现,绝大多数启动在2s以内,有一定的量分布在10s以上。10s以上认定为不太正常的统计数据,通过onWindowFocusChanged方式无法适配Push唤醒,黑屏等场景。下一步的想法是只统计launcher程序启动APP时长,忽略其他唤醒方式,异常数据可能会大量减少。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/29f3ac36.png\"> </p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>[1] art dex2oat 加载加速浅析:<a href=\"https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/\">https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/</a><br>[2] Android端应用秒开优化体验:<a href=\"http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/\">http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/</a><br>[3] atlas:<a href=\"https://github.com/alibaba/atlas\">https://github.com/alibaba/atlas</a> </p>\n","excerpt":"<h1 id=\"1、引言\"><a href=\"#1、引言\" class=\"headerlink\" title=\"1、引言\"></a>1、引言</h1><p>插件化是一把双刃剑,引入插件化实现后,每个APP都会面临插件框架带来的启动性能问题。性能问题不局限于以下所列项:</p>\n<ol>\n<li>ART Runtime上首次启动,dex2aot耗时问题; </li>\n<li>内致的静态插件和部分SDK,启动时作了大量的初始化操作,表现为Splash页停留5s+以上;</li>\n</ol>\n<p>微店也不例外,在插件化运行初期,微店APP仅静态插件加载时长超5s的每日量达2w+,另个一个店长版APP更不乐观。针对启动时长,我们尝试做了以下优化。</p>","more":"<h1 id=\"2、启动优化\"><a href=\"#2、启动优化\" class=\"headerlink\" title=\"2、启动优化\"></a>2、启动优化</h1><p>优化前,微店APP启动流程可简化成下图所示。APP在Applicaction的attachBaseContext,onCreate以及SplashActivity的onStart,onWindowFocusChanged函数中实现了插件的加载和SDK的初始化。 </p>\n<ol>\n<li>application attachBaseContext阶段:Muitidex处理后,在异步线程,插件框架收集所有静态插件信息(插件文件,插件组件解析)并进行loadDex等操作; </li>\n<li>application onCreate阶段:执行必要的插件以及核心SDK初始化代码,以保证后续功能调用成功,这里的核心SDK包括诸如网络,Crash上报库,埋点库等; </li>\n<li>activity onStart阶段:这里再细化为主线程和异步线程,执行部分SDK的初始化,比如Fresco;</li>\n<li>activity onWindowFocusChanged阶段:这一阶段执行不太紧急的SDK初始化,比如IM长连接,第三方统计库等。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/appstart.png\"></li>\n</ol>\n<p>在这一阶段,已经做到了SDK分层加载,但还是不太理想,主要表现在:</p>\n<ol>\n<li>application attachBaseContext阶段,所有插件都主动加载,并未按需加载或懒加载,比如App里的足迹插件,其实可以做为懒加载插件,在启动足迹页时,进行插件加载; </li>\n<li>application onCreate阶段,大量的SDK初始化,直接加长了启动时长,直接表现为用户看到的splash图其实是window背景,splash页一直在loading中;</li>\n<li>在Splash Activity周期函数中执行了SDK初始化代码,这里在绝大数场景下是没问题。但在自动化测试过程,可能不会先跳splash页,会引起由于SDK没初始化,导致异常表现。</li>\n</ol>\n<p>在收到华为市场启动时长过长警告后(华为要求电商APP启动时长不能超2s),决定优化启动流程。</p>\n<h1 id=\"3、优化设计\"><a href=\"#3、优化设计\" class=\"headerlink\" title=\"3、优化设计\"></a>3、优化设计</h1><p>针对上章节问题,对流程重构,设计了以下流程。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/appstart2.png\"><br>插件分为懒加载插件和非懒加载插件,启动时仅对非懒加载插件进行加载,懒加载插件仅做校验等工作。<br>在Application onCreate阶段,不再在主线程执行SDK初始化,在工作线程执行完核心SDK始化后,启动了多个其他工作线程对其余SDK进行操作;<br>在首个Activity onResume中对必须在主线程执行的SDK进行操作;</p>\n<h1 id=\"4、优化实现\"><a href=\"#4、优化实现\" class=\"headerlink\" title=\"4、优化实现\"></a>4、优化实现</h1><p>代码实现其实做了很多工作,非常优秀的团队小伙伴实现了包括ART上首次启动禁用dex2aot优化[1],应用秒开优化[2]。一些核心实现简述如下:</p>\n<ol>\n<li>ART首次启动优化,主要思路hook runtime->image_dex2oat_enabled_实现动态禁用dex2aot,由于Android版本多,rom包碎片化,这里涉及到大量的适配工作,主要在于runtime的偏移量矫正; </li>\n<li>在启动后,会在工作进程对所有插件进行dex2aot操作[3];</li>\n<li>APP hook instrumentation, 在第一个Activity实例化时,如果SDK init工作还未完成,先启动一个HookedActivity,等所有工作完成后,再启动业务Splash页。</li>\n</ol>\n<p>如果是拦截启动了hookedActivity,一定要在hookedInstrumentation.callActivityOnCreate方法重置theme为hookActivity自身主题,否则可能会抛出异常,原因我们可以看<a href=\"http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/app/ActivityThread.java#2808\">ActiviyThread.performLaunchActivity</a>的实现:</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">private</span> Activity <span class=\"title function_\">performLaunchActivity</span><span class=\"params\">(ActivityClientRecord r, Intent customIntent)</span> {</span><br><span class=\"line\">.....</span><br><span class=\"line\"> <span class=\"comment\">//r是ActivityRecord,r.activityInfo是从目标activity组件信息里读取的</span></span><br><span class=\"line\"> <span class=\"type\">int</span> <span class=\"variable\">theme</span> <span class=\"operator\">=</span> r.activityInfo.getThemeResource();</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (theme != <span class=\"number\">0</span>) {</span><br><span class=\"line\"> activity.setTheme(theme);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> activity.mCalled = <span class=\"literal\">false</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (r.isPersistable()) {</span><br><span class=\"line\"> mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> mInstrumentation.callActivityOnCreate(activity, r.state);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> </span><br><span class=\"line\">..... </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>经过一系列的优化后,我们拿到了比较理想的数据。从1w的采样数据我们发现,绝大多数启动在2s以内,有一定的量分布在10s以上。10s以上认定为不太正常的统计数据,通过onWindowFocusChanged方式无法适配Push唤醒,黑屏等场景。下一步的想法是只统计launcher程序启动APP时长,忽略其他唤醒方式,异常数据可能会大量减少。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/29f3ac36.png\"> </p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>[1] art dex2oat 加载加速浅析:<a href=\"https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/\">https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/</a><br>[2] Android端应用秒开优化体验:<a href=\"http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/\">http://zhengxiaoyong.com/2016/07/18/Android%E7%AB%AF%E5%BA%94%E7%94%A8%E7%A7%92%E5%BC%80%E4%BC%98%E5%8C%96%E4%BD%93%E9%AA%8C/</a><br>[3] atlas:<a href=\"https://github.com/alibaba/atlas\">https://github.com/alibaba/atlas</a> </p>"},{"title":"快速开闭闪光灯实现与风险规避","date":"2020-05-01T23:58:12.000Z","_content":"# 背景\n最近接手了一个有意思的需求,需求较为简单,在播放音乐时,在关键时间缀或时间段开闭闪光灯,实现音乐节奏和相机闪光联动。在Android上实现快速开闭闪光灯,避谈功耗问题,我们还需要考虑以下风险:\n* 避免APP丢帧,闪光和音乐的联动,必须异步线程处理;\n* 不能太过频发调用Camera,建议最小与系统vsync信号周期一致,即16ms;\n* Camera是共用的硬件资源,多摄像头设备上还需选取摄像头,使用中必须响应Camera或Torch回调;\n* 需要兼容手机厂商ROM包碎片化带来的一些问题,诸如魅族等设备与[AOSP](https://source.android.com/)实现有出入; \n\n在Android中,以Marshmallow(6.0)版本为界,开闭闪光有以下三种实现方案:\n\n| 名称 | 适用系统版本 | Camera 授权 |\n| ---- | ---- | ---- | \n| surface方式 | Marshmallow以下 | 不需要 |\n| preview方式 | Marshmallow以下 | 需要 |\n| torchMode方式 | Marshmallow及以上 | 不需要 |\n\n表格中的适用版本并不绝对,比如我们碰到了魅族有个7.0机型只适用preview方式。方案实现代码不复杂,我们可以参考AOSP FlashlightController实现。本文记录在研发过程发现的问题,避免其他团队重复入局踩坑。\n<!-- more -->\n\n# 代码实现\n使用闪光功能之前,我们要审明CAMERA和FLASHLIGHT权限、拿到可用Camera id以及监听Camera或Torch是否可用回调。\n审明权限:\n```\n //相关权限审明\n <uses-permission android:name=\"android.permission.CAMERA\" />\n <uses-permission android:name=\"android.permission.FLASHLIGHT\" />\n```\n获取可用Camera id:\n```\n //拿到具备闪光功能的后置摄像头Camera id\n private String getCameraId(Context mContext) throws CameraAccessException {\n CameraManager mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);\n String[] ids = mCameraManager.getCameraIdList();\n for (String id : ids) {\n CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);\n Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);\n Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);\n if (flashAvailable != null && flashAvailable\n && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {\n return id;\n }\n }\n return null;\n }\n\n```\nAndroid Marshmallow(6.0)以下监听Camera可用回调:\n```\n mCameraManager.registerAvailabilityCallback(mAvailabilityCallback, mHandler);\n /**\n * Register a callback to be notified about camera device availability.\n *\n * @param callback the new callback to send camera availability notices to\n * @param handler The handler on which the callback should be invoked, or {@code null} to use\n * the current thread's {@link android.os.Looper looper}.\n */\n public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,@Nullable Handler handler) \n```\nAndroid Marshmallow(6.0)及以上监听Torch可用回调:\n```\n mCameraManager.registerTorchCallback(mTorchCallback, mHandler);\n /**\n * Register a callback to be notified about torch mode status.\n *\n * @param callback The new callback to send torch mode status to\n * @param handler The handler on which the callback should be invoked, or {@code null} to use\n * the current thread's {@link android.os.Looper looper}.\n */\n public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler)\n```\n## Surface方式\nSurface方式在快速开闭使用场景下,其实不太适合,存在Surface与Camera输入纹理的浪费,但部分机型只能通过此方案使用,比如OPPO 5.x有个机型。参考[FlashlightController](http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java)实现,启动需要调用CameraManager#openCamera、CameraDevice#createCaptureSession、CameraCaptureSession#capture三个方法。\nCameraManager#openCamera:\n```\n private void startDevice() throws CameraAccessException {\n //cameraid, CameraDevice.StateCallback, handler\n mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler);\n }\n \n /**\n * Open a connection to a camera with the given ID.\n *\n * <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will\n * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up\n * for operation by calling {@link CameraDevice#createCaptureSession} and\n * {@link CameraDevice#createCaptureRequest}</p>\n *\n * <!--\n * <p>Since the camera device will be opened asynchronously, any asynchronous operations done\n * on the returned CameraDevice instance will be queued up until the device startup has\n * completed and the callback's {@link CameraDevice.StateCallback#onOpened onOpened} method is\n * called. The pending operations are then processed in order.</p>\n * -->\n * <p>If the camera becomes disconnected during initialization\n * after this function call returns,\n * {@link CameraDevice.StateCallback#onDisconnected} with a\n * {@link CameraDevice} in the disconnected state (and\n * {@link CameraDevice.StateCallback#onOpened} will be skipped).</p>\n *\n * <p>If opening the camera device fails, then the device callback's\n * {@link CameraDevice.StateCallback#onError onError} method will be called, and subsequent\n * calls on the camera device will throw a {@link CameraAccessException}.</p>\n *\n * @param cameraId\n * The unique identifier of the camera device to open\n * @param callback\n * The callback which is invoked once the camera is opened\n * @param handler\n * The handler on which the callback should be invoked, or\n * {@code null} to use the current thread's {@link android.os.Looper looper}.\n */\n @RequiresPermission(android.Manifest.permission.CAMERA)\n public void openCamera(@NonNull String cameraId,\n @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)\n\n```\n\n这里不对openCamera调用说明讲解,有个风险提醒,Camera是一个异步调用,所有请求在Camera Device打开之前,都会入队,成功后会处理请求队列,快速闪光请求下,可能会有延时响应或丢失风险。\nCameraDevice#createCaptureSession:\n```\n mSurface = new Surface(mSurfaceTexture);\n ArrayList<Surface> outputs = new ArrayList<>(1);\n outputs.add(mSurface);\n mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);\n \n /**\n * <p>Create a new camera capture session by providing the target output set of Surfaces to the\n * camera device.</p>\n *\n * @param outputs The new set of Surfaces that should be made available as\n * targets for captured image data.\n * @param callback The callback to notify about the status of the new capture session.\n * @param handler The handler on which the callback should be invoked, or {@code null} to use\n * the current thread's {@link android.os.Looper looper}.\n */\n public abstract void createCaptureSession(@NonNull List<Surface> outputs,\n @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)\n throws CameraAccessException;\n```\nCameraCaptureSession#capture:\n```\n CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(\n CameraDevice.TEMPLATE_PREVIEW);\n builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);\n builder.addTarget(mSurface);\n CaptureRequest request = builder.build();\n mSession.capture(request, null, getUsableHandler());\n mFlashlightRequest = request;\n \n /**\n * <p>Submit a request for an image to be captured by the camera device.</p>\n *\n * @param request the settings for this capture\n * @param listener The callback object to notify once this request has been\n * processed. If null, no metadata will be produced for this capture,\n * although image data will still be produced.\n * @param handler the handler on which the listener should be invoked, or\n * {@code null} to use the current thread's {@link android.os.Looper\n * looper}.\n *\n * @return int A unique capture sequence ID used by\n * {@link CaptureCallback#onCaptureSequenceCompleted}.\n */\n public abstract int capture(@NonNull CaptureRequest request,\n @Nullable CaptureCallback listener, @Nullable Handler handler)\n throws CameraAccessException; \n```\n关闭闪光灯较为简单,关闭CameraDivice,释放Surface实例等资源即可。\n```\n if (mCameraDevice != null) {\n mCameraDevice.close();\n teardown();\n }\n\n private void teardown() {\n mCameraDevice = null;\n mSession = null;\n mFlashlightRequest = null;\n if (mSurface != null) {\n mSurface.release();\n mSurfaceTexture.release();\n }\n mSurface = null;\n mSurfaceTexture = null;\n }\n```\n\n## Preview方式\n相比Surface方式,Preview性能消耗较小,但需动态授权Camera,部分魅族手机适用此方案。开启闪光需要调用Camera#open和Camera#startPreview方法。\n```\n private fun turnOn() {\n try {\n if (mCamera == null) {\n mCamera = Camera.open(mCameraId.toInt())\n }\n mCamera?.let {\n var parameters = it.getParameters()\n parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH)\n it.setParameters(parameters)\n it.startPreview()\n }\n } catch (e: Throwable) {\n dispatchError()\n }\n }\n /**\n * Creates a new Camera object to access a particular hardware camera. If\n * the same camera is opened by other applications, this will throw a\n * RuntimeException.\n *\n * <p>You must call {@link #release()} when you are done using the camera,\n * otherwise it will remain locked and be unavailable to other applications.\n *\n * <p>Your application should only have one Camera object active at a time\n * for a particular hardware camera.\n *\n * <p>Callbacks from other methods are delivered to the event loop of the\n * thread which called open(). If this thread has no event loop, then\n * callbacks are delivered to the main application event loop. If there\n * is no main application event loop, callbacks are not delivered.\n *\n * <p class=\"caution\"><b>Caution:</b> On some devices, this method may\n * take a long time to complete. It is best to call this method from a\n * worker thread (possibly using {@link android.os.AsyncTask}) to avoid\n * blocking the main application UI thread.\n *\n * @param cameraId the hardware camera to access, between 0 and\n * {@link #getNumberOfCameras()}-1.\n * @return a new Camera object, connected, locked and ready for use.\n */\n public static Camera open(int cameraId) {\n return new Camera(cameraId);\n }\n```\n关闭需要调用Camera#stopPreview和Camera#release方法。\n```\n private fun turnOff() {\n try {\n if (mCamera == null) {\n return\n }\n mCamera?.let {\n var parameters = it.getParameters()\n parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF)\n it.setParameters(parameters)\n it.stopPreview()\n it.release()\n }\n } catch (e: Throwable) {\n dispatchError()\n } finally {\n mCamera = null\n }\n }\n```\n## TorchMode方式\nTorchMode方式是Android在Marshmallow(6.0)及以上提供的闪光灯API,并且无需Camera动态授权,硬件响应及时。\n参考[FlashlightController](http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java)实现,开闭只需调用CameraManager#setTorchMode方法。\n```\n mCameraManager.setTorchMode(mCameraId, enabled);\n /**\n * Set the flash unit's torch mode of the camera of the given ID without opening the camera device.\n *\n * <p>Use {@link #getCameraIdList} to get the list of available camera devices and use\n * {@link #getCameraCharacteristics} to check whether the camera device has a flash unit.\n * Note that even if a camera device has a flash unit, turning on the torch mode may fail\n * if the camera device or other camera resources needed to turn on the torch mode are in use.\n * </p>\n *\n * <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully,\n * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked.\n * However, even if turning on the torch mode is successful, the application does not have the\n * exclusive ownership of the flash unit or the camera device. The torch mode will be turned\n * off and becomes unavailable when the camera device that the flash unit belongs to becomes\n * unavailable or when other camera resources to keep the torch on become unavailable (\n * {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also,\n * other applications are free to call {@link #setTorchMode} to turn off the torch mode (\n * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest\n * application that turned on the torch mode exits, the torch mode will be turned off.\n *\n * @param cameraId\n * The unique identifier of the camera device that the flash unit belongs to.\n * @param enabled\n * The desired state of the torch mode for the target camera device. Set to\n * {@code true} to turn on the torch mode. Set to {@code false} to turn off the\n * torch mode.\n */\n public void setTorchMode(@NonNull String cameraId, boolean enabled)\n```\n\n# 结束语\n我们在测试和灰度阶段收到部分不太理想的效果,这里列出:\n* 在实际测试过程发现,Marshmallow(6.0)以下部分设备,Surface和Preview方法均存在丢失或延时响应的现象,如果业务允许,建议可以直接屏蔽Marshmallow(6.0)以下设备;\n* Marshmallow(6.0)及以上,不是所有厂商都开启了Torch支持,例如魅族部分手机只支持Preview方式;\n* 国内部分厂商Lollipop(5.0)设备有自定义动态授权处理,调用前需要判断Camera是否禁用;\n* 国内部分厂商摄像头还有伸缩功能,快速开闭闪光灯,是一种很尴尬的使用方式;\n\n快速开闭闪光灯功能存在功耗较大和持续适配机型等问题,如非必要,不太建议实现类似本文中的需求和使用场景。\n# 参考\n1.FlashlightController 6.0.1:http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\n2.FlashlightController 5.1.1:http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\n","source":"_posts/快速开闭闪光灯实现与风险规避.md","raw":"---\ntitle: 快速开闭闪光灯实现与风险规避\ndate: 2020-05-02 07:58:12\ncategories: \n - Android\ntags: \n - 小功能组\n---\n# 背景\n最近接手了一个有意思的需求,需求较为简单,在播放音乐时,在关键时间缀或时间段开闭闪光灯,实现音乐节奏和相机闪光联动。在Android上实现快速开闭闪光灯,避谈功耗问题,我们还需要考虑以下风险:\n* 避免APP丢帧,闪光和音乐的联动,必须异步线程处理;\n* 不能太过频发调用Camera,建议最小与系统vsync信号周期一致,即16ms;\n* Camera是共用的硬件资源,多摄像头设备上还需选取摄像头,使用中必须响应Camera或Torch回调;\n* 需要兼容手机厂商ROM包碎片化带来的一些问题,诸如魅族等设备与[AOSP](https://source.android.com/)实现有出入; \n\n在Android中,以Marshmallow(6.0)版本为界,开闭闪光有以下三种实现方案:\n\n| 名称 | 适用系统版本 | Camera 授权 |\n| ---- | ---- | ---- | \n| surface方式 | Marshmallow以下 | 不需要 |\n| preview方式 | Marshmallow以下 | 需要 |\n| torchMode方式 | Marshmallow及以上 | 不需要 |\n\n表格中的适用版本并不绝对,比如我们碰到了魅族有个7.0机型只适用preview方式。方案实现代码不复杂,我们可以参考AOSP FlashlightController实现。本文记录在研发过程发现的问题,避免其他团队重复入局踩坑。\n<!-- more -->\n\n# 代码实现\n使用闪光功能之前,我们要审明CAMERA和FLASHLIGHT权限、拿到可用Camera id以及监听Camera或Torch是否可用回调。\n审明权限:\n```\n //相关权限审明\n <uses-permission android:name=\"android.permission.CAMERA\" />\n <uses-permission android:name=\"android.permission.FLASHLIGHT\" />\n```\n获取可用Camera id:\n```\n //拿到具备闪光功能的后置摄像头Camera id\n private String getCameraId(Context mContext) throws CameraAccessException {\n CameraManager mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);\n String[] ids = mCameraManager.getCameraIdList();\n for (String id : ids) {\n CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);\n Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);\n Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);\n if (flashAvailable != null && flashAvailable\n && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {\n return id;\n }\n }\n return null;\n }\n\n```\nAndroid Marshmallow(6.0)以下监听Camera可用回调:\n```\n mCameraManager.registerAvailabilityCallback(mAvailabilityCallback, mHandler);\n /**\n * Register a callback to be notified about camera device availability.\n *\n * @param callback the new callback to send camera availability notices to\n * @param handler The handler on which the callback should be invoked, or {@code null} to use\n * the current thread's {@link android.os.Looper looper}.\n */\n public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,@Nullable Handler handler) \n```\nAndroid Marshmallow(6.0)及以上监听Torch可用回调:\n```\n mCameraManager.registerTorchCallback(mTorchCallback, mHandler);\n /**\n * Register a callback to be notified about torch mode status.\n *\n * @param callback The new callback to send torch mode status to\n * @param handler The handler on which the callback should be invoked, or {@code null} to use\n * the current thread's {@link android.os.Looper looper}.\n */\n public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler)\n```\n## Surface方式\nSurface方式在快速开闭使用场景下,其实不太适合,存在Surface与Camera输入纹理的浪费,但部分机型只能通过此方案使用,比如OPPO 5.x有个机型。参考[FlashlightController](http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java)实现,启动需要调用CameraManager#openCamera、CameraDevice#createCaptureSession、CameraCaptureSession#capture三个方法。\nCameraManager#openCamera:\n```\n private void startDevice() throws CameraAccessException {\n //cameraid, CameraDevice.StateCallback, handler\n mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler);\n }\n \n /**\n * Open a connection to a camera with the given ID.\n *\n * <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will\n * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up\n * for operation by calling {@link CameraDevice#createCaptureSession} and\n * {@link CameraDevice#createCaptureRequest}</p>\n *\n * <!--\n * <p>Since the camera device will be opened asynchronously, any asynchronous operations done\n * on the returned CameraDevice instance will be queued up until the device startup has\n * completed and the callback's {@link CameraDevice.StateCallback#onOpened onOpened} method is\n * called. The pending operations are then processed in order.</p>\n * -->\n * <p>If the camera becomes disconnected during initialization\n * after this function call returns,\n * {@link CameraDevice.StateCallback#onDisconnected} with a\n * {@link CameraDevice} in the disconnected state (and\n * {@link CameraDevice.StateCallback#onOpened} will be skipped).</p>\n *\n * <p>If opening the camera device fails, then the device callback's\n * {@link CameraDevice.StateCallback#onError onError} method will be called, and subsequent\n * calls on the camera device will throw a {@link CameraAccessException}.</p>\n *\n * @param cameraId\n * The unique identifier of the camera device to open\n * @param callback\n * The callback which is invoked once the camera is opened\n * @param handler\n * The handler on which the callback should be invoked, or\n * {@code null} to use the current thread's {@link android.os.Looper looper}.\n */\n @RequiresPermission(android.Manifest.permission.CAMERA)\n public void openCamera(@NonNull String cameraId,\n @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)\n\n```\n\n这里不对openCamera调用说明讲解,有个风险提醒,Camera是一个异步调用,所有请求在Camera Device打开之前,都会入队,成功后会处理请求队列,快速闪光请求下,可能会有延时响应或丢失风险。\nCameraDevice#createCaptureSession:\n```\n mSurface = new Surface(mSurfaceTexture);\n ArrayList<Surface> outputs = new ArrayList<>(1);\n outputs.add(mSurface);\n mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);\n \n /**\n * <p>Create a new camera capture session by providing the target output set of Surfaces to the\n * camera device.</p>\n *\n * @param outputs The new set of Surfaces that should be made available as\n * targets for captured image data.\n * @param callback The callback to notify about the status of the new capture session.\n * @param handler The handler on which the callback should be invoked, or {@code null} to use\n * the current thread's {@link android.os.Looper looper}.\n */\n public abstract void createCaptureSession(@NonNull List<Surface> outputs,\n @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)\n throws CameraAccessException;\n```\nCameraCaptureSession#capture:\n```\n CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(\n CameraDevice.TEMPLATE_PREVIEW);\n builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);\n builder.addTarget(mSurface);\n CaptureRequest request = builder.build();\n mSession.capture(request, null, getUsableHandler());\n mFlashlightRequest = request;\n \n /**\n * <p>Submit a request for an image to be captured by the camera device.</p>\n *\n * @param request the settings for this capture\n * @param listener The callback object to notify once this request has been\n * processed. If null, no metadata will be produced for this capture,\n * although image data will still be produced.\n * @param handler the handler on which the listener should be invoked, or\n * {@code null} to use the current thread's {@link android.os.Looper\n * looper}.\n *\n * @return int A unique capture sequence ID used by\n * {@link CaptureCallback#onCaptureSequenceCompleted}.\n */\n public abstract int capture(@NonNull CaptureRequest request,\n @Nullable CaptureCallback listener, @Nullable Handler handler)\n throws CameraAccessException; \n```\n关闭闪光灯较为简单,关闭CameraDivice,释放Surface实例等资源即可。\n```\n if (mCameraDevice != null) {\n mCameraDevice.close();\n teardown();\n }\n\n private void teardown() {\n mCameraDevice = null;\n mSession = null;\n mFlashlightRequest = null;\n if (mSurface != null) {\n mSurface.release();\n mSurfaceTexture.release();\n }\n mSurface = null;\n mSurfaceTexture = null;\n }\n```\n\n## Preview方式\n相比Surface方式,Preview性能消耗较小,但需动态授权Camera,部分魅族手机适用此方案。开启闪光需要调用Camera#open和Camera#startPreview方法。\n```\n private fun turnOn() {\n try {\n if (mCamera == null) {\n mCamera = Camera.open(mCameraId.toInt())\n }\n mCamera?.let {\n var parameters = it.getParameters()\n parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH)\n it.setParameters(parameters)\n it.startPreview()\n }\n } catch (e: Throwable) {\n dispatchError()\n }\n }\n /**\n * Creates a new Camera object to access a particular hardware camera. If\n * the same camera is opened by other applications, this will throw a\n * RuntimeException.\n *\n * <p>You must call {@link #release()} when you are done using the camera,\n * otherwise it will remain locked and be unavailable to other applications.\n *\n * <p>Your application should only have one Camera object active at a time\n * for a particular hardware camera.\n *\n * <p>Callbacks from other methods are delivered to the event loop of the\n * thread which called open(). If this thread has no event loop, then\n * callbacks are delivered to the main application event loop. If there\n * is no main application event loop, callbacks are not delivered.\n *\n * <p class=\"caution\"><b>Caution:</b> On some devices, this method may\n * take a long time to complete. It is best to call this method from a\n * worker thread (possibly using {@link android.os.AsyncTask}) to avoid\n * blocking the main application UI thread.\n *\n * @param cameraId the hardware camera to access, between 0 and\n * {@link #getNumberOfCameras()}-1.\n * @return a new Camera object, connected, locked and ready for use.\n */\n public static Camera open(int cameraId) {\n return new Camera(cameraId);\n }\n```\n关闭需要调用Camera#stopPreview和Camera#release方法。\n```\n private fun turnOff() {\n try {\n if (mCamera == null) {\n return\n }\n mCamera?.let {\n var parameters = it.getParameters()\n parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF)\n it.setParameters(parameters)\n it.stopPreview()\n it.release()\n }\n } catch (e: Throwable) {\n dispatchError()\n } finally {\n mCamera = null\n }\n }\n```\n## TorchMode方式\nTorchMode方式是Android在Marshmallow(6.0)及以上提供的闪光灯API,并且无需Camera动态授权,硬件响应及时。\n参考[FlashlightController](http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java)实现,开闭只需调用CameraManager#setTorchMode方法。\n```\n mCameraManager.setTorchMode(mCameraId, enabled);\n /**\n * Set the flash unit's torch mode of the camera of the given ID without opening the camera device.\n *\n * <p>Use {@link #getCameraIdList} to get the list of available camera devices and use\n * {@link #getCameraCharacteristics} to check whether the camera device has a flash unit.\n * Note that even if a camera device has a flash unit, turning on the torch mode may fail\n * if the camera device or other camera resources needed to turn on the torch mode are in use.\n * </p>\n *\n * <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully,\n * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked.\n * However, even if turning on the torch mode is successful, the application does not have the\n * exclusive ownership of the flash unit or the camera device. The torch mode will be turned\n * off and becomes unavailable when the camera device that the flash unit belongs to becomes\n * unavailable or when other camera resources to keep the torch on become unavailable (\n * {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also,\n * other applications are free to call {@link #setTorchMode} to turn off the torch mode (\n * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest\n * application that turned on the torch mode exits, the torch mode will be turned off.\n *\n * @param cameraId\n * The unique identifier of the camera device that the flash unit belongs to.\n * @param enabled\n * The desired state of the torch mode for the target camera device. Set to\n * {@code true} to turn on the torch mode. Set to {@code false} to turn off the\n * torch mode.\n */\n public void setTorchMode(@NonNull String cameraId, boolean enabled)\n```\n\n# 结束语\n我们在测试和灰度阶段收到部分不太理想的效果,这里列出:\n* 在实际测试过程发现,Marshmallow(6.0)以下部分设备,Surface和Preview方法均存在丢失或延时响应的现象,如果业务允许,建议可以直接屏蔽Marshmallow(6.0)以下设备;\n* Marshmallow(6.0)及以上,不是所有厂商都开启了Torch支持,例如魅族部分手机只支持Preview方式;\n* 国内部分厂商Lollipop(5.0)设备有自定义动态授权处理,调用前需要判断Camera是否禁用;\n* 国内部分厂商摄像头还有伸缩功能,快速开闭闪光灯,是一种很尴尬的使用方式;\n\n快速开闭闪光灯功能存在功耗较大和持续适配机型等问题,如非必要,不太建议实现类似本文中的需求和使用场景。\n# 参考\n1.FlashlightController 6.0.1:http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\n2.FlashlightController 5.1.1:http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\n","slug":"快速开闭闪光灯实现与风险规避","published":1,"updated":"2025-06-02T13:15:33.845Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n8a000ncate2adveb92","content":"<h1 id=\"背景\"><a href=\"#背景\" class=\"headerlink\" title=\"背景\"></a>背景</h1><p>最近接手了一个有意思的需求,需求较为简单,在播放音乐时,在关键时间缀或时间段开闭闪光灯,实现音乐节奏和相机闪光联动。在Android上实现快速开闭闪光灯,避谈功耗问题,我们还需要考虑以下风险:</p>\n<ul>\n<li>避免APP丢帧,闪光和音乐的联动,必须异步线程处理;</li>\n<li>不能太过频发调用Camera,建议最小与系统vsync信号周期一致,即16ms;</li>\n<li>Camera是共用的硬件资源,多摄像头设备上还需选取摄像头,使用中必须响应Camera或Torch回调;</li>\n<li>需要兼容手机厂商ROM包碎片化带来的一些问题,诸如魅族等设备与<a href=\"https://source.android.com/\">AOSP</a>实现有出入;</li>\n</ul>\n<p>在Android中,以Marshmallow(6.0)版本为界,开闭闪光有以下三种实现方案:</p>\n<table>\n<thead>\n<tr>\n<th>名称</th>\n<th>适用系统版本</th>\n<th>Camera 授权</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>surface方式</td>\n<td>Marshmallow以下</td>\n<td>不需要</td>\n</tr>\n<tr>\n<td>preview方式</td>\n<td>Marshmallow以下</td>\n<td>需要</td>\n</tr>\n<tr>\n<td>torchMode方式</td>\n<td>Marshmallow及以上</td>\n<td>不需要</td>\n</tr>\n</tbody></table>\n<p>表格中的适用版本并不绝对,比如我们碰到了魅族有个7.0机型只适用preview方式。方案实现代码不复杂,我们可以参考AOSP FlashlightController实现。本文记录在研发过程发现的问题,避免其他团队重复入局踩坑。</p>\n<span id=\"more\"></span>\n\n<h1 id=\"代码实现\"><a href=\"#代码实现\" class=\"headerlink\" title=\"代码实现\"></a>代码实现</h1><p>使用闪光功能之前,我们要审明CAMERA和FLASHLIGHT权限、拿到可用Camera id以及监听Camera或Torch是否可用回调。<br>审明权限:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//相关权限审明</span><br><span class=\"line\"><uses-permission android:name="android.permission.CAMERA" /></span><br><span class=\"line\"><uses-permission android:name="android.permission.FLASHLIGHT" /></span><br></pre></td></tr></table></figure>\n<p>获取可用Camera id:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//拿到具备闪光功能的后置摄像头Camera id</span><br><span class=\"line\">private String getCameraId(Context mContext) throws CameraAccessException {</span><br><span class=\"line\"> CameraManager mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);</span><br><span class=\"line\"> String[] ids = mCameraManager.getCameraIdList();</span><br><span class=\"line\"> for (String id : ids) {</span><br><span class=\"line\"> CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);</span><br><span class=\"line\"> Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);</span><br><span class=\"line\"> Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);</span><br><span class=\"line\"> if (flashAvailable != null && flashAvailable</span><br><span class=\"line\"> && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {</span><br><span class=\"line\"> return id;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return null;</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n<p>Android Marshmallow(6.0)以下监听Camera可用回调:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"> mCameraManager.registerAvailabilityCallback(mAvailabilityCallback, mHandler);</span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * Register a callback to be notified about camera device availability.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param callback the new callback to send camera availability notices to</span><br><span class=\"line\"> * @param handler The handler on which the callback should be invoked, or {@code null} to use</span><br><span class=\"line\"> * the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,@Nullable Handler handler) </span><br></pre></td></tr></table></figure>\n<p>Android Marshmallow(6.0)及以上监听Torch可用回调:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">mCameraManager.registerTorchCallback(mTorchCallback, mHandler);</span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Register a callback to be notified about torch mode status.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param callback The new callback to send torch mode status to</span><br><span class=\"line\"> * @param handler The handler on which the callback should be invoked, or {@code null} to use</span><br><span class=\"line\"> * the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler)</span><br></pre></td></tr></table></figure>\n<h2 id=\"Surface方式\"><a href=\"#Surface方式\" class=\"headerlink\" title=\"Surface方式\"></a>Surface方式</h2><p>Surface方式在快速开闭使用场景下,其实不太适合,存在Surface与Camera输入纹理的浪费,但部分机型只能通过此方案使用,比如OPPO 5.x有个机型。参考<a href=\"http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">FlashlightController</a>实现,启动需要调用CameraManager#openCamera、CameraDevice#createCaptureSession、CameraCaptureSession#capture三个方法。<br>CameraManager#openCamera:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">private void startDevice() throws CameraAccessException {</span><br><span class=\"line\"> //cameraid, CameraDevice.StateCallback, handler</span><br><span class=\"line\"> mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Open a connection to a camera with the given ID.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will</span><br><span class=\"line\"> * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up</span><br><span class=\"line\"> * for operation by calling {@link CameraDevice#createCaptureSession} and</span><br><span class=\"line\"> * {@link CameraDevice#createCaptureRequest}</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <!--</span><br><span class=\"line\"> * <p>Since the camera device will be opened asynchronously, any asynchronous operations done</span><br><span class=\"line\"> * on the returned CameraDevice instance will be queued up until the device startup has</span><br><span class=\"line\"> * completed and the callback's {@link CameraDevice.StateCallback#onOpened onOpened} method is</span><br><span class=\"line\"> * called. The pending operations are then processed in order.</p></span><br><span class=\"line\"> * --></span><br><span class=\"line\"> * <p>If the camera becomes disconnected during initialization</span><br><span class=\"line\"> * after this function call returns,</span><br><span class=\"line\"> * {@link CameraDevice.StateCallback#onDisconnected} with a</span><br><span class=\"line\"> * {@link CameraDevice} in the disconnected state (and</span><br><span class=\"line\"> * {@link CameraDevice.StateCallback#onOpened} will be skipped).</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>If opening the camera device fails, then the device callback's</span><br><span class=\"line\"> * {@link CameraDevice.StateCallback#onError onError} method will be called, and subsequent</span><br><span class=\"line\"> * calls on the camera device will throw a {@link CameraAccessException}.</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param cameraId</span><br><span class=\"line\"> * The unique identifier of the camera device to open</span><br><span class=\"line\"> * @param callback</span><br><span class=\"line\"> * The callback which is invoked once the camera is opened</span><br><span class=\"line\"> * @param handler</span><br><span class=\"line\"> * The handler on which the callback should be invoked, or</span><br><span class=\"line\"> * {@code null} to use the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">@RequiresPermission(android.Manifest.permission.CAMERA)</span><br><span class=\"line\">public void openCamera(@NonNull String cameraId,</span><br><span class=\"line\"> @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)</span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n\n<p>这里不对openCamera调用说明讲解,有个风险提醒,Camera是一个异步调用,所有请求在Camera Device打开之前,都会入队,成功后会处理请求队列,快速闪光请求下,可能会有延时响应或丢失风险。<br>CameraDevice#createCaptureSession:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">mSurface = new Surface(mSurfaceTexture);</span><br><span class=\"line\">ArrayList<Surface> outputs = new ArrayList<>(1);</span><br><span class=\"line\">outputs.add(mSurface);</span><br><span class=\"line\">mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * <p>Create a new camera capture session by providing the target output set of Surfaces to the</span><br><span class=\"line\"> * camera device.</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param outputs The new set of Surfaces that should be made available as</span><br><span class=\"line\"> * targets for captured image data.</span><br><span class=\"line\"> * @param callback The callback to notify about the status of the new capture session.</span><br><span class=\"line\"> * @param handler The handler on which the callback should be invoked, or {@code null} to use</span><br><span class=\"line\"> * the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public abstract void createCaptureSession(@NonNull List<Surface> outputs,</span><br><span class=\"line\"> @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)</span><br><span class=\"line\"> throws CameraAccessException;</span><br></pre></td></tr></table></figure>\n<p>CameraCaptureSession#capture:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(</span><br><span class=\"line\"> CameraDevice.TEMPLATE_PREVIEW);</span><br><span class=\"line\">builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);</span><br><span class=\"line\">builder.addTarget(mSurface);</span><br><span class=\"line\">CaptureRequest request = builder.build();</span><br><span class=\"line\">mSession.capture(request, null, getUsableHandler());</span><br><span class=\"line\">mFlashlightRequest = request;</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * <p>Submit a request for an image to be captured by the camera device.</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param request the settings for this capture</span><br><span class=\"line\"> * @param listener The callback object to notify once this request has been</span><br><span class=\"line\"> * processed. If null, no metadata will be produced for this capture,</span><br><span class=\"line\"> * although image data will still be produced.</span><br><span class=\"line\"> * @param handler the handler on which the listener should be invoked, or</span><br><span class=\"line\"> * {@code null} to use the current thread's {@link android.os.Looper</span><br><span class=\"line\"> * looper}.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @return int A unique capture sequence ID used by</span><br><span class=\"line\"> * {@link CaptureCallback#onCaptureSequenceCompleted}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public abstract int capture(@NonNull CaptureRequest request,</span><br><span class=\"line\"> @Nullable CaptureCallback listener, @Nullable Handler handler)</span><br><span class=\"line\"> throws CameraAccessException; </span><br></pre></td></tr></table></figure>\n<p>关闭闪光灯较为简单,关闭CameraDivice,释放Surface实例等资源即可。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">if (mCameraDevice != null) {</span><br><span class=\"line\"> mCameraDevice.close();</span><br><span class=\"line\"> teardown();</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\">private void teardown() {</span><br><span class=\"line\"> mCameraDevice = null;</span><br><span class=\"line\"> mSession = null;</span><br><span class=\"line\"> mFlashlightRequest = null;</span><br><span class=\"line\"> if (mSurface != null) {</span><br><span class=\"line\"> mSurface.release();</span><br><span class=\"line\"> mSurfaceTexture.release();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> mSurface = null;</span><br><span class=\"line\"> mSurfaceTexture = null;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"Preview方式\"><a href=\"#Preview方式\" class=\"headerlink\" title=\"Preview方式\"></a>Preview方式</h2><p>相比Surface方式,Preview性能消耗较小,但需动态授权Camera,部分魅族手机适用此方案。开启闪光需要调用Camera#open和Camera#startPreview方法。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">private fun turnOn() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> if (mCamera == null) {</span><br><span class=\"line\"> mCamera = Camera.open(mCameraId.toInt())</span><br><span class=\"line\"> }</span><br><span class=\"line\"> mCamera?.let {</span><br><span class=\"line\"> var parameters = it.getParameters()</span><br><span class=\"line\"> parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH)</span><br><span class=\"line\"> it.setParameters(parameters)</span><br><span class=\"line\"> it.startPreview()</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } catch (e: Throwable) {</span><br><span class=\"line\"> dispatchError()</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Creates a new Camera object to access a particular hardware camera. If</span><br><span class=\"line\"> * the same camera is opened by other applications, this will throw a</span><br><span class=\"line\"> * RuntimeException.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>You must call {@link #release()} when you are done using the camera,</span><br><span class=\"line\"> * otherwise it will remain locked and be unavailable to other applications.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Your application should only have one Camera object active at a time</span><br><span class=\"line\"> * for a particular hardware camera.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Callbacks from other methods are delivered to the event loop of the</span><br><span class=\"line\"> * thread which called open(). If this thread has no event loop, then</span><br><span class=\"line\"> * callbacks are delivered to the main application event loop. If there</span><br><span class=\"line\"> * is no main application event loop, callbacks are not delivered.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p class="caution"><b>Caution:</b> On some devices, this method may</span><br><span class=\"line\"> * take a long time to complete. It is best to call this method from a</span><br><span class=\"line\"> * worker thread (possibly using {@link android.os.AsyncTask}) to avoid</span><br><span class=\"line\"> * blocking the main application UI thread.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param cameraId the hardware camera to access, between 0 and</span><br><span class=\"line\"> * {@link #getNumberOfCameras()}-1.</span><br><span class=\"line\"> * @return a new Camera object, connected, locked and ready for use.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public static Camera open(int cameraId) {</span><br><span class=\"line\"> return new Camera(cameraId);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>关闭需要调用Camera#stopPreview和Camera#release方法。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">private fun turnOff() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> if (mCamera == null) {</span><br><span class=\"line\"> return</span><br><span class=\"line\"> }</span><br><span class=\"line\"> mCamera?.let {</span><br><span class=\"line\"> var parameters = it.getParameters()</span><br><span class=\"line\"> parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF)</span><br><span class=\"line\"> it.setParameters(parameters)</span><br><span class=\"line\"> it.stopPreview()</span><br><span class=\"line\"> it.release()</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } catch (e: Throwable) {</span><br><span class=\"line\"> dispatchError()</span><br><span class=\"line\"> } finally {</span><br><span class=\"line\"> mCamera = null</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h2 id=\"TorchMode方式\"><a href=\"#TorchMode方式\" class=\"headerlink\" title=\"TorchMode方式\"></a>TorchMode方式</h2><p>TorchMode方式是Android在Marshmallow(6.0)及以上提供的闪光灯API,并且无需Camera动态授权,硬件响应及时。<br>参考<a href=\"http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">FlashlightController</a>实现,开闭只需调用CameraManager#setTorchMode方法。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"> mCameraManager.setTorchMode(mCameraId, enabled);</span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Set the flash unit's torch mode of the camera of the given ID without opening the camera device.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Use {@link #getCameraIdList} to get the list of available camera devices and use</span><br><span class=\"line\"> * {@link #getCameraCharacteristics} to check whether the camera device has a flash unit.</span><br><span class=\"line\"> * Note that even if a camera device has a flash unit, turning on the torch mode may fail</span><br><span class=\"line\"> * if the camera device or other camera resources needed to turn on the torch mode are in use.</span><br><span class=\"line\"> * </p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully,</span><br><span class=\"line\"> * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked.</span><br><span class=\"line\"> * However, even if turning on the torch mode is successful, the application does not have the</span><br><span class=\"line\"> * exclusive ownership of the flash unit or the camera device. The torch mode will be turned</span><br><span class=\"line\"> * off and becomes unavailable when the camera device that the flash unit belongs to becomes</span><br><span class=\"line\"> * unavailable or when other camera resources to keep the torch on become unavailable (</span><br><span class=\"line\"> * {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also,</span><br><span class=\"line\"> * other applications are free to call {@link #setTorchMode} to turn off the torch mode (</span><br><span class=\"line\"> * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest</span><br><span class=\"line\"> * application that turned on the torch mode exits, the torch mode will be turned off.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param cameraId</span><br><span class=\"line\"> * The unique identifier of the camera device that the flash unit belongs to.</span><br><span class=\"line\"> * @param enabled</span><br><span class=\"line\"> * The desired state of the torch mode for the target camera device. Set to</span><br><span class=\"line\"> * {@code true} to turn on the torch mode. Set to {@code false} to turn off the</span><br><span class=\"line\"> * torch mode.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public void setTorchMode(@NonNull String cameraId, boolean enabled)</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>我们在测试和灰度阶段收到部分不太理想的效果,这里列出:</p>\n<ul>\n<li>在实际测试过程发现,Marshmallow(6.0)以下部分设备,Surface和Preview方法均存在丢失或延时响应的现象,如果业务允许,建议可以直接屏蔽Marshmallow(6.0)以下设备;</li>\n<li>Marshmallow(6.0)及以上,不是所有厂商都开启了Torch支持,例如魅族部分手机只支持Preview方式;</li>\n<li>国内部分厂商Lollipop(5.0)设备有自定义动态授权处理,调用前需要判断Camera是否禁用;</li>\n<li>国内部分厂商摄像头还有伸缩功能,快速开闭闪光灯,是一种很尴尬的使用方式;</li>\n</ul>\n<p>快速开闭闪光灯功能存在功耗较大和持续适配机型等问题,如非必要,不太建议实现类似本文中的需求和使用场景。</p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>1.FlashlightController 6.0.1:<a href=\"http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java</a><br>2.FlashlightController 5.1.1:<a href=\"http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java</a></p>\n","excerpt":"<h1 id=\"背景\"><a href=\"#背景\" class=\"headerlink\" title=\"背景\"></a>背景</h1><p>最近接手了一个有意思的需求,需求较为简单,在播放音乐时,在关键时间缀或时间段开闭闪光灯,实现音乐节奏和相机闪光联动。在Android上实现快速开闭闪光灯,避谈功耗问题,我们还需要考虑以下风险:</p>\n<ul>\n<li>避免APP丢帧,闪光和音乐的联动,必须异步线程处理;</li>\n<li>不能太过频发调用Camera,建议最小与系统vsync信号周期一致,即16ms;</li>\n<li>Camera是共用的硬件资源,多摄像头设备上还需选取摄像头,使用中必须响应Camera或Torch回调;</li>\n<li>需要兼容手机厂商ROM包碎片化带来的一些问题,诸如魅族等设备与<a href=\"https://source.android.com/\">AOSP</a>实现有出入;</li>\n</ul>\n<p>在Android中,以Marshmallow(6.0)版本为界,开闭闪光有以下三种实现方案:</p>\n<table>\n<thead>\n<tr>\n<th>名称</th>\n<th>适用系统版本</th>\n<th>Camera 授权</th>\n</tr>\n</thead>\n<tbody><tr>\n<td>surface方式</td>\n<td>Marshmallow以下</td>\n<td>不需要</td>\n</tr>\n<tr>\n<td>preview方式</td>\n<td>Marshmallow以下</td>\n<td>需要</td>\n</tr>\n<tr>\n<td>torchMode方式</td>\n<td>Marshmallow及以上</td>\n<td>不需要</td>\n</tr>\n</tbody></table>\n<p>表格中的适用版本并不绝对,比如我们碰到了魅族有个7.0机型只适用preview方式。方案实现代码不复杂,我们可以参考AOSP FlashlightController实现。本文记录在研发过程发现的问题,避免其他团队重复入局踩坑。</p>","more":"<h1 id=\"代码实现\"><a href=\"#代码实现\" class=\"headerlink\" title=\"代码实现\"></a>代码实现</h1><p>使用闪光功能之前,我们要审明CAMERA和FLASHLIGHT权限、拿到可用Camera id以及监听Camera或Torch是否可用回调。<br>审明权限:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//相关权限审明</span><br><span class=\"line\"><uses-permission android:name="android.permission.CAMERA" /></span><br><span class=\"line\"><uses-permission android:name="android.permission.FLASHLIGHT" /></span><br></pre></td></tr></table></figure>\n<p>获取可用Camera id:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//拿到具备闪光功能的后置摄像头Camera id</span><br><span class=\"line\">private String getCameraId(Context mContext) throws CameraAccessException {</span><br><span class=\"line\"> CameraManager mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);</span><br><span class=\"line\"> String[] ids = mCameraManager.getCameraIdList();</span><br><span class=\"line\"> for (String id : ids) {</span><br><span class=\"line\"> CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);</span><br><span class=\"line\"> Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);</span><br><span class=\"line\"> Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);</span><br><span class=\"line\"> if (flashAvailable != null && flashAvailable</span><br><span class=\"line\"> && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {</span><br><span class=\"line\"> return id;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return null;</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n<p>Android Marshmallow(6.0)以下监听Camera可用回调:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"> mCameraManager.registerAvailabilityCallback(mAvailabilityCallback, mHandler);</span><br><span class=\"line\"> /**</span><br><span class=\"line\"> * Register a callback to be notified about camera device availability.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param callback the new callback to send camera availability notices to</span><br><span class=\"line\"> * @param handler The handler on which the callback should be invoked, or {@code null} to use</span><br><span class=\"line\"> * the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public void registerAvailabilityCallback(@NonNull AvailabilityCallback callback,@Nullable Handler handler) </span><br></pre></td></tr></table></figure>\n<p>Android Marshmallow(6.0)及以上监听Torch可用回调:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">mCameraManager.registerTorchCallback(mTorchCallback, mHandler);</span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Register a callback to be notified about torch mode status.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param callback The new callback to send torch mode status to</span><br><span class=\"line\"> * @param handler The handler on which the callback should be invoked, or {@code null} to use</span><br><span class=\"line\"> * the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public void registerTorchCallback(@NonNull TorchCallback callback, @Nullable Handler handler)</span><br></pre></td></tr></table></figure>\n<h2 id=\"Surface方式\"><a href=\"#Surface方式\" class=\"headerlink\" title=\"Surface方式\"></a>Surface方式</h2><p>Surface方式在快速开闭使用场景下,其实不太适合,存在Surface与Camera输入纹理的浪费,但部分机型只能通过此方案使用,比如OPPO 5.x有个机型。参考<a href=\"http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">FlashlightController</a>实现,启动需要调用CameraManager#openCamera、CameraDevice#createCaptureSession、CameraCaptureSession#capture三个方法。<br>CameraManager#openCamera:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">private void startDevice() throws CameraAccessException {</span><br><span class=\"line\"> //cameraid, CameraDevice.StateCallback, handler</span><br><span class=\"line\"> mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Open a connection to a camera with the given ID.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Once the camera is successfully opened, {@link CameraDevice.StateCallback#onOpened} will</span><br><span class=\"line\"> * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up</span><br><span class=\"line\"> * for operation by calling {@link CameraDevice#createCaptureSession} and</span><br><span class=\"line\"> * {@link CameraDevice#createCaptureRequest}</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <!--</span><br><span class=\"line\"> * <p>Since the camera device will be opened asynchronously, any asynchronous operations done</span><br><span class=\"line\"> * on the returned CameraDevice instance will be queued up until the device startup has</span><br><span class=\"line\"> * completed and the callback's {@link CameraDevice.StateCallback#onOpened onOpened} method is</span><br><span class=\"line\"> * called. The pending operations are then processed in order.</p></span><br><span class=\"line\"> * --></span><br><span class=\"line\"> * <p>If the camera becomes disconnected during initialization</span><br><span class=\"line\"> * after this function call returns,</span><br><span class=\"line\"> * {@link CameraDevice.StateCallback#onDisconnected} with a</span><br><span class=\"line\"> * {@link CameraDevice} in the disconnected state (and</span><br><span class=\"line\"> * {@link CameraDevice.StateCallback#onOpened} will be skipped).</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>If opening the camera device fails, then the device callback's</span><br><span class=\"line\"> * {@link CameraDevice.StateCallback#onError onError} method will be called, and subsequent</span><br><span class=\"line\"> * calls on the camera device will throw a {@link CameraAccessException}.</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param cameraId</span><br><span class=\"line\"> * The unique identifier of the camera device to open</span><br><span class=\"line\"> * @param callback</span><br><span class=\"line\"> * The callback which is invoked once the camera is opened</span><br><span class=\"line\"> * @param handler</span><br><span class=\"line\"> * The handler on which the callback should be invoked, or</span><br><span class=\"line\"> * {@code null} to use the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">@RequiresPermission(android.Manifest.permission.CAMERA)</span><br><span class=\"line\">public void openCamera(@NonNull String cameraId,</span><br><span class=\"line\"> @NonNull final CameraDevice.StateCallback callback, @Nullable Handler handler)</span><br><span class=\"line\"></span><br></pre></td></tr></table></figure>\n\n<p>这里不对openCamera调用说明讲解,有个风险提醒,Camera是一个异步调用,所有请求在Camera Device打开之前,都会入队,成功后会处理请求队列,快速闪光请求下,可能会有延时响应或丢失风险。<br>CameraDevice#createCaptureSession:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">mSurface = new Surface(mSurfaceTexture);</span><br><span class=\"line\">ArrayList<Surface> outputs = new ArrayList<>(1);</span><br><span class=\"line\">outputs.add(mSurface);</span><br><span class=\"line\">mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * <p>Create a new camera capture session by providing the target output set of Surfaces to the</span><br><span class=\"line\"> * camera device.</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param outputs The new set of Surfaces that should be made available as</span><br><span class=\"line\"> * targets for captured image data.</span><br><span class=\"line\"> * @param callback The callback to notify about the status of the new capture session.</span><br><span class=\"line\"> * @param handler The handler on which the callback should be invoked, or {@code null} to use</span><br><span class=\"line\"> * the current thread's {@link android.os.Looper looper}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public abstract void createCaptureSession(@NonNull List<Surface> outputs,</span><br><span class=\"line\"> @NonNull CameraCaptureSession.StateCallback callback, @Nullable Handler handler)</span><br><span class=\"line\"> throws CameraAccessException;</span><br></pre></td></tr></table></figure>\n<p>CameraCaptureSession#capture:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(</span><br><span class=\"line\"> CameraDevice.TEMPLATE_PREVIEW);</span><br><span class=\"line\">builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);</span><br><span class=\"line\">builder.addTarget(mSurface);</span><br><span class=\"line\">CaptureRequest request = builder.build();</span><br><span class=\"line\">mSession.capture(request, null, getUsableHandler());</span><br><span class=\"line\">mFlashlightRequest = request;</span><br><span class=\"line\"></span><br><span class=\"line\">/**</span><br><span class=\"line\"> * <p>Submit a request for an image to be captured by the camera device.</p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param request the settings for this capture</span><br><span class=\"line\"> * @param listener The callback object to notify once this request has been</span><br><span class=\"line\"> * processed. If null, no metadata will be produced for this capture,</span><br><span class=\"line\"> * although image data will still be produced.</span><br><span class=\"line\"> * @param handler the handler on which the listener should be invoked, or</span><br><span class=\"line\"> * {@code null} to use the current thread's {@link android.os.Looper</span><br><span class=\"line\"> * looper}.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @return int A unique capture sequence ID used by</span><br><span class=\"line\"> * {@link CaptureCallback#onCaptureSequenceCompleted}.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public abstract int capture(@NonNull CaptureRequest request,</span><br><span class=\"line\"> @Nullable CaptureCallback listener, @Nullable Handler handler)</span><br><span class=\"line\"> throws CameraAccessException; </span><br></pre></td></tr></table></figure>\n<p>关闭闪光灯较为简单,关闭CameraDivice,释放Surface实例等资源即可。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">if (mCameraDevice != null) {</span><br><span class=\"line\"> mCameraDevice.close();</span><br><span class=\"line\"> teardown();</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\">private void teardown() {</span><br><span class=\"line\"> mCameraDevice = null;</span><br><span class=\"line\"> mSession = null;</span><br><span class=\"line\"> mFlashlightRequest = null;</span><br><span class=\"line\"> if (mSurface != null) {</span><br><span class=\"line\"> mSurface.release();</span><br><span class=\"line\"> mSurfaceTexture.release();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> mSurface = null;</span><br><span class=\"line\"> mSurfaceTexture = null;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<h2 id=\"Preview方式\"><a href=\"#Preview方式\" class=\"headerlink\" title=\"Preview方式\"></a>Preview方式</h2><p>相比Surface方式,Preview性能消耗较小,但需动态授权Camera,部分魅族手机适用此方案。开启闪光需要调用Camera#open和Camera#startPreview方法。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">private fun turnOn() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> if (mCamera == null) {</span><br><span class=\"line\"> mCamera = Camera.open(mCameraId.toInt())</span><br><span class=\"line\"> }</span><br><span class=\"line\"> mCamera?.let {</span><br><span class=\"line\"> var parameters = it.getParameters()</span><br><span class=\"line\"> parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH)</span><br><span class=\"line\"> it.setParameters(parameters)</span><br><span class=\"line\"> it.startPreview()</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } catch (e: Throwable) {</span><br><span class=\"line\"> dispatchError()</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Creates a new Camera object to access a particular hardware camera. If</span><br><span class=\"line\"> * the same camera is opened by other applications, this will throw a</span><br><span class=\"line\"> * RuntimeException.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>You must call {@link #release()} when you are done using the camera,</span><br><span class=\"line\"> * otherwise it will remain locked and be unavailable to other applications.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Your application should only have one Camera object active at a time</span><br><span class=\"line\"> * for a particular hardware camera.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Callbacks from other methods are delivered to the event loop of the</span><br><span class=\"line\"> * thread which called open(). If this thread has no event loop, then</span><br><span class=\"line\"> * callbacks are delivered to the main application event loop. If there</span><br><span class=\"line\"> * is no main application event loop, callbacks are not delivered.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p class="caution"><b>Caution:</b> On some devices, this method may</span><br><span class=\"line\"> * take a long time to complete. It is best to call this method from a</span><br><span class=\"line\"> * worker thread (possibly using {@link android.os.AsyncTask}) to avoid</span><br><span class=\"line\"> * blocking the main application UI thread.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param cameraId the hardware camera to access, between 0 and</span><br><span class=\"line\"> * {@link #getNumberOfCameras()}-1.</span><br><span class=\"line\"> * @return a new Camera object, connected, locked and ready for use.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public static Camera open(int cameraId) {</span><br><span class=\"line\"> return new Camera(cameraId);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>关闭需要调用Camera#stopPreview和Camera#release方法。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">private fun turnOff() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> if (mCamera == null) {</span><br><span class=\"line\"> return</span><br><span class=\"line\"> }</span><br><span class=\"line\"> mCamera?.let {</span><br><span class=\"line\"> var parameters = it.getParameters()</span><br><span class=\"line\"> parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF)</span><br><span class=\"line\"> it.setParameters(parameters)</span><br><span class=\"line\"> it.stopPreview()</span><br><span class=\"line\"> it.release()</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } catch (e: Throwable) {</span><br><span class=\"line\"> dispatchError()</span><br><span class=\"line\"> } finally {</span><br><span class=\"line\"> mCamera = null</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h2 id=\"TorchMode方式\"><a href=\"#TorchMode方式\" class=\"headerlink\" title=\"TorchMode方式\"></a>TorchMode方式</h2><p>TorchMode方式是Android在Marshmallow(6.0)及以上提供的闪光灯API,并且无需Camera动态授权,硬件响应及时。<br>参考<a href=\"http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">FlashlightController</a>实现,开闭只需调用CameraManager#setTorchMode方法。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"> mCameraManager.setTorchMode(mCameraId, enabled);</span><br><span class=\"line\">/**</span><br><span class=\"line\"> * Set the flash unit's torch mode of the camera of the given ID without opening the camera device.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p>Use {@link #getCameraIdList} to get the list of available camera devices and use</span><br><span class=\"line\"> * {@link #getCameraCharacteristics} to check whether the camera device has a flash unit.</span><br><span class=\"line\"> * Note that even if a camera device has a flash unit, turning on the torch mode may fail</span><br><span class=\"line\"> * if the camera device or other camera resources needed to turn on the torch mode are in use.</span><br><span class=\"line\"> * </p></span><br><span class=\"line\"> *</span><br><span class=\"line\"> * <p> If {@link #setTorchMode} is called to turn on or off the torch mode successfully,</span><br><span class=\"line\"> * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked.</span><br><span class=\"line\"> * However, even if turning on the torch mode is successful, the application does not have the</span><br><span class=\"line\"> * exclusive ownership of the flash unit or the camera device. The torch mode will be turned</span><br><span class=\"line\"> * off and becomes unavailable when the camera device that the flash unit belongs to becomes</span><br><span class=\"line\"> * unavailable or when other camera resources to keep the torch on become unavailable (</span><br><span class=\"line\"> * {@link CameraManager.TorchCallback#onTorchModeUnavailable} will be invoked). Also,</span><br><span class=\"line\"> * other applications are free to call {@link #setTorchMode} to turn off the torch mode (</span><br><span class=\"line\"> * {@link CameraManager.TorchCallback#onTorchModeChanged} will be invoked). If the latest</span><br><span class=\"line\"> * application that turned on the torch mode exits, the torch mode will be turned off.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param cameraId</span><br><span class=\"line\"> * The unique identifier of the camera device that the flash unit belongs to.</span><br><span class=\"line\"> * @param enabled</span><br><span class=\"line\"> * The desired state of the torch mode for the target camera device. Set to</span><br><span class=\"line\"> * {@code true} to turn on the torch mode. Set to {@code false} to turn off the</span><br><span class=\"line\"> * torch mode.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public void setTorchMode(@NonNull String cameraId, boolean enabled)</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>我们在测试和灰度阶段收到部分不太理想的效果,这里列出:</p>\n<ul>\n<li>在实际测试过程发现,Marshmallow(6.0)以下部分设备,Surface和Preview方法均存在丢失或延时响应的现象,如果业务允许,建议可以直接屏蔽Marshmallow(6.0)以下设备;</li>\n<li>Marshmallow(6.0)及以上,不是所有厂商都开启了Torch支持,例如魅族部分手机只支持Preview方式;</li>\n<li>国内部分厂商Lollipop(5.0)设备有自定义动态授权处理,调用前需要判断Camera是否禁用;</li>\n<li>国内部分厂商摄像头还有伸缩功能,快速开闭闪光灯,是一种很尴尬的使用方式;</li>\n</ul>\n<p>快速开闭闪光灯功能存在功耗较大和持续适配机型等问题,如非必要,不太建议实现类似本文中的需求和使用场景。</p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>1.FlashlightController 6.0.1:<a href=\"http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">http://androidxref.com/6.0.1_r10/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java</a><br>2.FlashlightController 5.1.1:<a href=\"http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java\">http://androidxref.com/5.1.1_r6/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java</a></p>"},{"title":"插件化之插件混淆的可行性探索","date":"2019-05-23T10:49:25.000Z","_content":"# 1、引言\n为提高研发效能、业务迭代速度、版本覆盖率等指标,业界选择插件化解决方案的团队不在少数,并且有很好的输出,如手淘,滴滴,爱奇艺。方案要在效率,质量和安全等方面取得平衡。反编译APK发现,很多团队会对插件代码不作混淆处理,其中有一些考虑因素,诸如方便动态部署,快速实现版本覆盖等。如何在兼顾动态部署等需要的同时,对插件代码甚至资源进行混淆,加大逆向分析成本,提高代码安全,我司在去年进行了尝试和探索,并取得了一定的成效。\n<!-- more -->\n# 2、插件混淆\nAndroid插件化,核心在于复杂业务下,宿主和插件类、组件、资源加载等实现,其次是围绕核心实现的诸如插件发包,动态部署,插件混淆,容器安全和监控等周边建设。下图是微店插件化架构示意图。\n \n在微店插件化架构中,插件其实是一个APK,与atlas[1]类似,我司把插件伪装成SO包形式,放至APK libs目录中。要实现N+1个 APK(N个插件+1个宿主)的代码混淆一致性,存在以下约束: \n1. 保持插件之间相关API类代码不被混淆,一个简单的例子,商品详情插件依赖购物车插件相关API类,这些API类最好能不被混淆,否则可能引起类查找等问题; \n2. 保持一些三方库或基础SDK(微店把三方库和基础SDK暂时放至了宿主中,同时被插件进行provided依赖)被插件引用到的API类最好能不被混淆或apply同份mapping文件,否则可能引起类查找等问题; \n3. 混淆后的代码,N+1个插件不能存在重复类名,方法和属性,否则会引起类查找等问题; 针对上述问题,我们进行了可行性探索,下面给出实践方案。\n\n### 2.1 保持插件之间相关API类代码不被混淆\n保持插件API类代码不被混淆,原始的方法是让业务开发,手动编写progurd配置,这样会带来业务开发效率低下问题。我们的做法是编写一个注解类如@Export,注解了@Export的类会在混淆时进行自动keep。\n```java\n// Export class \n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Keep\npublic @interface Export {\n}\n// proguard file \n-keep,allowobfuscation @interface com.weidian.framework.annotation.Export\n-keep @com.weidian.framework.annotation.Export class *{*;}\n```\n\n### 2.2 保持三方库或基础SDK引用类代码不被混淆\n与插件API类不同,三方库与基础SDK,除了SDK自身声明的混淆规则外,插件可能还会引用可以被混淆的类。在常规架构下,可以被混淆的类,放至插件化场景下,这些类最好被KEEP或N+1个插件apply同份mapping文件。为基础SDK配置@Export注解不太合适,会存在反向依赖,限制了SDK功能以及无法处理三方库。为兼顾效率与质量,我们做了下以尝试: \n1.编写自动扫描插件源码脚本,为源码中import的类自动生成keep混淆声明,并让APP打包时,包含这些自动生成的混淆脚本。(现通过Dexdeps方式查找插件中引用的非打进包中的类和方法[2]) \n2.编写Gradle Task,在processReleaseResources Task后拿到proguardOutputFile,这个文件中包含了插件在layout中声明的view混淆规则,并让APP打包时,包含这些自动生成的混淆脚本。 \n```java\n def processResourceTask=project.tasks.getByName(\"processReleaseResources\")\n if (processResourceTask) {\n \t\t processResourceTask.doLast {\n \t\t File rules = processResourceTask.getProguardOutputFile()\n \t\t if (rules.exists()) {\n \t\t //export file\n \t\t }\n }\n \t }\n```\n\n在实践过程,我们发现,如果能让N+1个插件apply同份mapping文件 ,APK的大小能减少3-4m左右,不过考虑到其他因素,我们暂时采用了KEEP类代码方案。 \n\n### 2.3 插件混淆后的类代码唯一\n保持插件混淆后的类代码唯一性,这个其实progurad[3]已经提供了支持,采用-defaultpackage 或-flattenpackagehierarchy都可以解决代码混淆后冲突问题。我们采用了-flattenpackagehierarchy方式,为每个插件混淆类加入包名前缀。\n\n```\n-defaultpackage renamepakcage 将混淆的类的包名替换\n-flattenpackagehierarchye prepackage 将混淆的类包名加上前缀包名\n```\n\n# 参考\n[1] atlas:[https://github.com/alibaba/atlas](https://github.com/alibaba/atlas) \n[2] dexdeps:[https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt](https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt) \n[3] proguard:[https://www.guardsquare.com/en/products/proguard/manual/usage](https://www.guardsquare.com/en/products/proguard/manual/usage) \n\n ","source":"_posts/插件化之插件混淆的可行性探索.md","raw":"---\ntitle: 插件化之插件混淆的可行性探索\ndate: 2019-05-23 18:49:25\ncategories: \n\t- Android\ntags: \n\t- 插件化\n---\n# 1、引言\n为提高研发效能、业务迭代速度、版本覆盖率等指标,业界选择插件化解决方案的团队不在少数,并且有很好的输出,如手淘,滴滴,爱奇艺。方案要在效率,质量和安全等方面取得平衡。反编译APK发现,很多团队会对插件代码不作混淆处理,其中有一些考虑因素,诸如方便动态部署,快速实现版本覆盖等。如何在兼顾动态部署等需要的同时,对插件代码甚至资源进行混淆,加大逆向分析成本,提高代码安全,我司在去年进行了尝试和探索,并取得了一定的成效。\n<!-- more -->\n# 2、插件混淆\nAndroid插件化,核心在于复杂业务下,宿主和插件类、组件、资源加载等实现,其次是围绕核心实现的诸如插件发包,动态部署,插件混淆,容器安全和监控等周边建设。下图是微店插件化架构示意图。\n \n在微店插件化架构中,插件其实是一个APK,与atlas[1]类似,我司把插件伪装成SO包形式,放至APK libs目录中。要实现N+1个 APK(N个插件+1个宿主)的代码混淆一致性,存在以下约束: \n1. 保持插件之间相关API类代码不被混淆,一个简单的例子,商品详情插件依赖购物车插件相关API类,这些API类最好能不被混淆,否则可能引起类查找等问题; \n2. 保持一些三方库或基础SDK(微店把三方库和基础SDK暂时放至了宿主中,同时被插件进行provided依赖)被插件引用到的API类最好能不被混淆或apply同份mapping文件,否则可能引起类查找等问题; \n3. 混淆后的代码,N+1个插件不能存在重复类名,方法和属性,否则会引起类查找等问题; 针对上述问题,我们进行了可行性探索,下面给出实践方案。\n\n### 2.1 保持插件之间相关API类代码不被混淆\n保持插件API类代码不被混淆,原始的方法是让业务开发,手动编写progurd配置,这样会带来业务开发效率低下问题。我们的做法是编写一个注解类如@Export,注解了@Export的类会在混淆时进行自动keep。\n```java\n// Export class \n@Target(ElementType.TYPE)\n@Retention(RetentionPolicy.RUNTIME)\n@Keep\npublic @interface Export {\n}\n// proguard file \n-keep,allowobfuscation @interface com.weidian.framework.annotation.Export\n-keep @com.weidian.framework.annotation.Export class *{*;}\n```\n\n### 2.2 保持三方库或基础SDK引用类代码不被混淆\n与插件API类不同,三方库与基础SDK,除了SDK自身声明的混淆规则外,插件可能还会引用可以被混淆的类。在常规架构下,可以被混淆的类,放至插件化场景下,这些类最好被KEEP或N+1个插件apply同份mapping文件。为基础SDK配置@Export注解不太合适,会存在反向依赖,限制了SDK功能以及无法处理三方库。为兼顾效率与质量,我们做了下以尝试: \n1.编写自动扫描插件源码脚本,为源码中import的类自动生成keep混淆声明,并让APP打包时,包含这些自动生成的混淆脚本。(现通过Dexdeps方式查找插件中引用的非打进包中的类和方法[2]) \n2.编写Gradle Task,在processReleaseResources Task后拿到proguardOutputFile,这个文件中包含了插件在layout中声明的view混淆规则,并让APP打包时,包含这些自动生成的混淆脚本。 \n```java\n def processResourceTask=project.tasks.getByName(\"processReleaseResources\")\n if (processResourceTask) {\n \t\t processResourceTask.doLast {\n \t\t File rules = processResourceTask.getProguardOutputFile()\n \t\t if (rules.exists()) {\n \t\t //export file\n \t\t }\n }\n \t }\n```\n\n在实践过程,我们发现,如果能让N+1个插件apply同份mapping文件 ,APK的大小能减少3-4m左右,不过考虑到其他因素,我们暂时采用了KEEP类代码方案。 \n\n### 2.3 插件混淆后的类代码唯一\n保持插件混淆后的类代码唯一性,这个其实progurad[3]已经提供了支持,采用-defaultpackage 或-flattenpackagehierarchy都可以解决代码混淆后冲突问题。我们采用了-flattenpackagehierarchy方式,为每个插件混淆类加入包名前缀。\n\n```\n-defaultpackage renamepakcage 将混淆的类的包名替换\n-flattenpackagehierarchye prepackage 将混淆的类包名加上前缀包名\n```\n\n# 参考\n[1] atlas:[https://github.com/alibaba/atlas](https://github.com/alibaba/atlas) \n[2] dexdeps:[https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt](https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt) \n[3] proguard:[https://www.guardsquare.com/en/products/proguard/manual/usage](https://www.guardsquare.com/en/products/proguard/manual/usage) \n\n ","slug":"插件化之插件混淆的可行性探索","published":1,"updated":"2025-06-02T13:15:33.846Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n8b000qcategt6m150u","content":"<h1 id=\"1、引言\"><a href=\"#1、引言\" class=\"headerlink\" title=\"1、引言\"></a>1、引言</h1><p>为提高研发效能、业务迭代速度、版本覆盖率等指标,业界选择插件化解决方案的团队不在少数,并且有很好的输出,如手淘,滴滴,爱奇艺。方案要在效率,质量和安全等方面取得平衡。反编译APK发现,很多团队会对插件代码不作混淆处理,其中有一些考虑因素,诸如方便动态部署,快速实现版本覆盖等。如何在兼顾动态部署等需要的同时,对插件代码甚至资源进行混淆,加大逆向分析成本,提高代码安全,我司在去年进行了尝试和探索,并取得了一定的成效。</p>\n<span id=\"more\"></span>\n<h1 id=\"2、插件混淆\"><a href=\"#2、插件混淆\" class=\"headerlink\" title=\"2、插件混淆\"></a>2、插件混淆</h1><p>Android插件化,核心在于复杂业务下,宿主和插件类、组件、资源加载等实现,其次是围绕核心实现的诸如插件发包,动态部署,插件混淆,容器安全和监控等周边建设。下图是微店插件化架构示意图。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/b6f8dc13.png\"><br>在微店插件化架构中,插件其实是一个APK,与atlas[1]类似,我司把插件伪装成SO包形式,放至APK libs目录中。要实现N+1个 APK(N个插件+1个宿主)的代码混淆一致性,存在以下约束: </p>\n<ol>\n<li>保持插件之间相关API类代码不被混淆,一个简单的例子,商品详情插件依赖购物车插件相关API类,这些API类最好能不被混淆,否则可能引起类查找等问题; </li>\n<li>保持一些三方库或基础SDK(微店把三方库和基础SDK暂时放至了宿主中,同时被插件进行provided依赖)被插件引用到的API类最好能不被混淆或apply同份mapping文件,否则可能引起类查找等问题; </li>\n<li>混淆后的代码,N+1个插件不能存在重复类名,方法和属性,否则会引起类查找等问题; 针对上述问题,我们进行了可行性探索,下面给出实践方案。</li>\n</ol>\n<h3 id=\"2-1-保持插件之间相关API类代码不被混淆\"><a href=\"#2-1-保持插件之间相关API类代码不被混淆\" class=\"headerlink\" title=\"2.1 保持插件之间相关API类代码不被混淆\"></a>2.1 保持插件之间相关API类代码不被混淆</h3><p>保持插件API类代码不被混淆,原始的方法是让业务开发,手动编写progurd配置,这样会带来业务开发效率低下问题。我们的做法是编写一个注解类如@Export,注解了@Export的类会在混淆时进行自动keep。</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// Export class </span></span><br><span class=\"line\"><span class=\"meta\">@Target(ElementType.TYPE)</span></span><br><span class=\"line\"><span class=\"meta\">@Retention(RetentionPolicy.RUNTIME)</span></span><br><span class=\"line\"><span class=\"meta\">@Keep</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"meta\">@interface</span> Export {</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">// proguard file </span></span><br><span class=\"line\">-keep,allowobfuscation <span class=\"meta\">@interface</span> com.weidian.framework.annotation.Export</span><br><span class=\"line\">-keep <span class=\"meta\">@com</span>.weidian.framework.annotation.Export class *{*;}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"2-2-保持三方库或基础SDK引用类代码不被混淆\"><a href=\"#2-2-保持三方库或基础SDK引用类代码不被混淆\" class=\"headerlink\" title=\"2.2 保持三方库或基础SDK引用类代码不被混淆\"></a>2.2 保持三方库或基础SDK引用类代码不被混淆</h3><p>与插件API类不同,三方库与基础SDK,除了SDK自身声明的混淆规则外,插件可能还会引用可以被混淆的类。在常规架构下,可以被混淆的类,放至插件化场景下,这些类最好被KEEP或N+1个插件apply同份mapping文件。为基础SDK配置@Export注解不太合适,会存在反向依赖,限制了SDK功能以及无法处理三方库。为兼顾效率与质量,我们做了下以尝试:<br>1.编写自动扫描插件源码脚本,为源码中import的类自动生成keep混淆声明,并让APP打包时,包含这些自动生成的混淆脚本。(现通过Dexdeps方式查找插件中引用的非打进包中的类和方法[2])<br>2.编写Gradle Task,在processReleaseResources Task后拿到proguardOutputFile,这个文件中包含了插件在layout中声明的view混淆规则,并让APP打包时,包含这些自动生成的混淆脚本。 </p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">def processResourceTask=project.tasks.getByName(<span class=\"string\">"processReleaseResources"</span>)</span><br><span class=\"line\"><span class=\"keyword\">if</span> (processResourceTask) {</span><br><span class=\"line\">\t\t processResourceTask.doLast {</span><br><span class=\"line\">\t\t <span class=\"type\">File</span> <span class=\"variable\">rules</span> <span class=\"operator\">=</span> processResourceTask.getProguardOutputFile()</span><br><span class=\"line\">\t\t <span class=\"keyword\">if</span> (rules.exists()) {</span><br><span class=\"line\">\t\t <span class=\"comment\">//export file</span></span><br><span class=\"line\">\t\t }</span><br><span class=\"line\"> }</span><br><span class=\"line\">\t }</span><br></pre></td></tr></table></figure>\n\n<p>在实践过程,我们发现,如果能让N+1个插件apply同份mapping文件 ,APK的大小能减少3-4m左右,不过考虑到其他因素,我们暂时采用了KEEP类代码方案。 </p>\n<h3 id=\"2-3-插件混淆后的类代码唯一\"><a href=\"#2-3-插件混淆后的类代码唯一\" class=\"headerlink\" title=\"2.3 插件混淆后的类代码唯一\"></a>2.3 插件混淆后的类代码唯一</h3><p>保持插件混淆后的类代码唯一性,这个其实progurad[3]已经提供了支持,采用-defaultpackage 或-flattenpackagehierarchy都可以解决代码混淆后冲突问题。我们采用了-flattenpackagehierarchy方式,为每个插件混淆类加入包名前缀。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">-defaultpackage renamepakcage 将混淆的类的包名替换</span><br><span class=\"line\">-flattenpackagehierarchye prepackage 将混淆的类包名加上前缀包名</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>[1] atlas:<a href=\"https://github.com/alibaba/atlas\">https://github.com/alibaba/atlas</a><br>[2] dexdeps:<a href=\"https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt\">https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt</a><br>[3] proguard:<a href=\"https://www.guardsquare.com/en/products/proguard/manual/usage\">https://www.guardsquare.com/en/products/proguard/manual/usage</a> </p>\n","excerpt":"<h1 id=\"1、引言\"><a href=\"#1、引言\" class=\"headerlink\" title=\"1、引言\"></a>1、引言</h1><p>为提高研发效能、业务迭代速度、版本覆盖率等指标,业界选择插件化解决方案的团队不在少数,并且有很好的输出,如手淘,滴滴,爱奇艺。方案要在效率,质量和安全等方面取得平衡。反编译APK发现,很多团队会对插件代码不作混淆处理,其中有一些考虑因素,诸如方便动态部署,快速实现版本覆盖等。如何在兼顾动态部署等需要的同时,对插件代码甚至资源进行混淆,加大逆向分析成本,提高代码安全,我司在去年进行了尝试和探索,并取得了一定的成效。</p>","more":"<h1 id=\"2、插件混淆\"><a href=\"#2、插件混淆\" class=\"headerlink\" title=\"2、插件混淆\"></a>2、插件混淆</h1><p>Android插件化,核心在于复杂业务下,宿主和插件类、组件、资源加载等实现,其次是围绕核心实现的诸如插件发包,动态部署,插件混淆,容器安全和监控等周边建设。下图是微店插件化架构示意图。<br><img src=\"https://raw.githubusercontent.com/emile2013/emile2013.github.io/source/source/imgs/b6f8dc13.png\"><br>在微店插件化架构中,插件其实是一个APK,与atlas[1]类似,我司把插件伪装成SO包形式,放至APK libs目录中。要实现N+1个 APK(N个插件+1个宿主)的代码混淆一致性,存在以下约束: </p>\n<ol>\n<li>保持插件之间相关API类代码不被混淆,一个简单的例子,商品详情插件依赖购物车插件相关API类,这些API类最好能不被混淆,否则可能引起类查找等问题; </li>\n<li>保持一些三方库或基础SDK(微店把三方库和基础SDK暂时放至了宿主中,同时被插件进行provided依赖)被插件引用到的API类最好能不被混淆或apply同份mapping文件,否则可能引起类查找等问题; </li>\n<li>混淆后的代码,N+1个插件不能存在重复类名,方法和属性,否则会引起类查找等问题; 针对上述问题,我们进行了可行性探索,下面给出实践方案。</li>\n</ol>\n<h3 id=\"2-1-保持插件之间相关API类代码不被混淆\"><a href=\"#2-1-保持插件之间相关API类代码不被混淆\" class=\"headerlink\" title=\"2.1 保持插件之间相关API类代码不被混淆\"></a>2.1 保持插件之间相关API类代码不被混淆</h3><p>保持插件API类代码不被混淆,原始的方法是让业务开发,手动编写progurd配置,这样会带来业务开发效率低下问题。我们的做法是编写一个注解类如@Export,注解了@Export的类会在混淆时进行自动keep。</p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// Export class </span></span><br><span class=\"line\"><span class=\"meta\">@Target(ElementType.TYPE)</span></span><br><span class=\"line\"><span class=\"meta\">@Retention(RetentionPolicy.RUNTIME)</span></span><br><span class=\"line\"><span class=\"meta\">@Keep</span></span><br><span class=\"line\"><span class=\"keyword\">public</span> <span class=\"meta\">@interface</span> Export {</span><br><span class=\"line\">}</span><br><span class=\"line\"><span class=\"comment\">// proguard file </span></span><br><span class=\"line\">-keep,allowobfuscation <span class=\"meta\">@interface</span> com.weidian.framework.annotation.Export</span><br><span class=\"line\">-keep <span class=\"meta\">@com</span>.weidian.framework.annotation.Export class *{*;}</span><br></pre></td></tr></table></figure>\n\n<h3 id=\"2-2-保持三方库或基础SDK引用类代码不被混淆\"><a href=\"#2-2-保持三方库或基础SDK引用类代码不被混淆\" class=\"headerlink\" title=\"2.2 保持三方库或基础SDK引用类代码不被混淆\"></a>2.2 保持三方库或基础SDK引用类代码不被混淆</h3><p>与插件API类不同,三方库与基础SDK,除了SDK自身声明的混淆规则外,插件可能还会引用可以被混淆的类。在常规架构下,可以被混淆的类,放至插件化场景下,这些类最好被KEEP或N+1个插件apply同份mapping文件。为基础SDK配置@Export注解不太合适,会存在反向依赖,限制了SDK功能以及无法处理三方库。为兼顾效率与质量,我们做了下以尝试:<br>1.编写自动扫描插件源码脚本,为源码中import的类自动生成keep混淆声明,并让APP打包时,包含这些自动生成的混淆脚本。(现通过Dexdeps方式查找插件中引用的非打进包中的类和方法[2])<br>2.编写Gradle Task,在processReleaseResources Task后拿到proguardOutputFile,这个文件中包含了插件在layout中声明的view混淆规则,并让APP打包时,包含这些自动生成的混淆脚本。 </p>\n<figure class=\"highlight java\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">def processResourceTask=project.tasks.getByName(<span class=\"string\">"processReleaseResources"</span>)</span><br><span class=\"line\"><span class=\"keyword\">if</span> (processResourceTask) {</span><br><span class=\"line\">\t\t processResourceTask.doLast {</span><br><span class=\"line\">\t\t <span class=\"type\">File</span> <span class=\"variable\">rules</span> <span class=\"operator\">=</span> processResourceTask.getProguardOutputFile()</span><br><span class=\"line\">\t\t <span class=\"keyword\">if</span> (rules.exists()) {</span><br><span class=\"line\">\t\t <span class=\"comment\">//export file</span></span><br><span class=\"line\">\t\t }</span><br><span class=\"line\"> }</span><br><span class=\"line\">\t }</span><br></pre></td></tr></table></figure>\n\n<p>在实践过程,我们发现,如果能让N+1个插件apply同份mapping文件 ,APK的大小能减少3-4m左右,不过考虑到其他因素,我们暂时采用了KEEP类代码方案。 </p>\n<h3 id=\"2-3-插件混淆后的类代码唯一\"><a href=\"#2-3-插件混淆后的类代码唯一\" class=\"headerlink\" title=\"2.3 插件混淆后的类代码唯一\"></a>2.3 插件混淆后的类代码唯一</h3><p>保持插件混淆后的类代码唯一性,这个其实progurad[3]已经提供了支持,采用-defaultpackage 或-flattenpackagehierarchy都可以解决代码混淆后冲突问题。我们采用了-flattenpackagehierarchy方式,为每个插件混淆类加入包名前缀。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">-defaultpackage renamepakcage 将混淆的类的包名替换</span><br><span class=\"line\">-flattenpackagehierarchye prepackage 将混淆的类包名加上前缀包名</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>[1] atlas:<a href=\"https://github.com/alibaba/atlas\">https://github.com/alibaba/atlas</a><br>[2] dexdeps:<a href=\"https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt\">https://android.googlesource.com/platform/dalvik.git/+/master/tools/dexdeps/README.txt</a><br>[3] proguard:<a href=\"https://www.guardsquare.com/en/products/proguard/manual/usage\">https://www.guardsquare.com/en/products/proguard/manual/usage</a> </p>"},{"title":"治理令人头痛的pthread_create OutOfMemoryError错误","date":"2019-09-20T09:44:10.000Z","_content":"# 引言\n我相信很多团队都面对过令人头痛的pthread_create 创建线程内存溢出问题。在Android中,典型的pthread_create内存溢出堆栈信息如下:\n\n```\n//此异常多为栈内存分配失败\njava.lang.OutOfMemoryError\npthread_create (1040KB stack) failed: Try again\n1 java.lang.Thread.nativeCreate(Native Method)\n2 java.lang.Thread.start(Thread.java:733)\n3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975)\n4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043)\n5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185)\n6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n7 java.lang.Thread.run(Thread.java:764)\n```\n\n```\n//此异常多为线程数到达上限\njava.lang.OutOfMemoryError\npthread_create (1040KB stack) failed: Out of memory\n1 java.lang.Thread.nativeCreate(Native Method)\n2 java.lang.Thread.start(Thread.java:743)\n3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)\n4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)\n5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)\n6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)\n7 java.lang.Thread.run(Thread.java:774)\n```\n\n\n出现创建线程内存溢出无非两个原因: \n1、进程的栈内存超过了虚拟机的最大内存数; \n2、线程数达到了系统最大限制数; \n关于线程数达到了系统最大限制数,在国内手机厂商中,华为手机在7.0+手机上已将最大线程数修改成了300。我们APP有大量的华为用户,不得不面对华为系统限制问题。 \nAndroid Dalvik和ART,将stack分为了java stack和native stack,本文没去具体实验,从[kongxinsun](https://blog.csdn.net/kongxinsun/article/details/78679860)的博客,我们了解到两者总量是1056KB。栈内存回收和堆内存策略不一样,比较简单,当线程结束,线程占用的栈内存也就回收了。 \n<!-- more -->\n接下来通过简述Android中常见的使用线程方式,尝试给出解决方案,优化线程引起的性能问题。\n# 线程使用\n在Android中,我们使用新建线程,无非就是想避免一些耗时操作,影响主线程响应。常见的相关类,比如Timer,ThreadHandle,AsyncTask,ThreadPoolExecutor等等都和线程直接相关。相关网络库、图片库、埋点库和其他三方SDK等都会存在大量使用线程场景。 \n查看当前所有激活的线程,我们可以通过以下API获取:\n```\n//输出总数\nFile file = new File(Environment.getExternalStorageDirectory(), \"threads.txt\");\nBufferedWriter writer = new BufferedWriter(new FileWriter(file, false));\nwriter.write(\"count:\" + Thread.getAllStackTraces().size() + \"\\n\");\nfor (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {\n writer.write(entry.getKey().getName() + \":\" + \"\\n\");\n for (StackTraceElement traceElement : entry.getValue()) {\n writer.write(\"\\tat \" + traceElement + \"\\n\");\n }\n}\n```\n\n```\n//输出内容\ncount:166\nConnectivityThread:\n at android.os.MessageQueue.nativePollOnce(Native Method)\n at android.os.MessageQueue.next(MessageQueue.java:336)\n at android.os.Looper.loop(Looper.java:174)\n at android.os.HandlerThread.run(HandlerThread.java:67)\npool-16-thread-1:\n at sun.misc.Unsafe.park(Native Method)\n at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)\n at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)\n at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)\n at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)\n at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)\n at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n at java.lang.Thread.run(Thread.java:919)\nBundleTaskExecutor #5:\n at sun.misc.Unsafe.park(Native Method)\n at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)\n at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:461)\n at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)\n at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:937)\n at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)\n at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)\n at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n at java.lang.Thread.run(Thread.java:919)\nexecute_task:\n at android.os.MessageQueue.nativePollOnce(Native Method)\n at android.os.MessageQueue.next(MessageQueue.java:336)\n at android.os.Looper.loop(Looper.java:174)\n at android.os.HandlerThread.run(HandlerThread.java:67)\nClassLoaderCreator #4:\n at sun.misc.Unsafe.park(Native Method)\n at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)\n at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2109)\n at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)\n at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)\n at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)\n at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n at java.lang.Thread.run(Thread.java:919)\n```\n这种方式我们能拿到Java层与其对接的native层thread总数,但是拿不到没有attach到java层的native thread,比如Futter engine中的native thread等。 \n如果为线程分配一个可定位到代码的名称,那我们完全能对症下药。但实际很难,我们没法约束开发同学和三方SDK为每个线程起自定义名称,例如上面的pool-16-thread-1的线程,我们很难定位到是哪个类发起的调用。 \n另外我们也可以直接dump进程内存,来查看内存情况,使用方式如下所示:\n```\nadb shell dumpsys meminfo [pacakgename]\n\nApplications Memory Usage (in Kilobytes):\nUptime: 122226380 Realtime: 451586500\n** MEMINFO in pid 19468 [pacakgename] **\n Pss Private Private SwapPss Heap Heap Heap\n Total Dirty Clean Dirty Size Alloc Free\n ------ ------ ------ ------ ------ ------ ------\n Native Heap 109473 109384 0 62 128460 122131 6328\n Dalvik Heap 17679 17604 0 63 26514 13257 13257\n Dalvik Other 5481 5480 0 0\n Stack 88 88 0 0\n Ashmem 326 124 0 0\n Gfx dev 55952 55952 0 0\n Other dev 216 0 216 0\n .so mmap 17532 1012 13224 21\n .jar mmap 2232 0 80 0\n .apk mmap 34716 132 19520 0\n .ttf mmap 199 0 140 0\n .dex mmap 45759 44872 492 0\n .oat mmap 213 0 80 0\n .art mmap 4888 4440 8 40\n Other mmap 9466 836 6200 0\n EGL mtrack 28572 28572 0 0\n GL mtrack 18996 18996 0 0\n Unknown 3572 3564 0 5\n TOTAL 355551 291056 39960 191 154974 135388 19585\n... \n```\n也可以查看线程等汇总数据,使用方式如下所示:\n```\nadb shell\nsargo:/ $ cat /proc/19468/status\nName: xxxx\nUmask: 0077\nState: S (sleeping)\nTgid: 19468\nNgid: 0\nPid: 19468\nPPid: 789\nTracerPid: 0\nUid: 10243 10243 10243 10243\nGid: 10243 10243 10243 10243\nFDSize: 512\nGroups: 3001 3002 3003 9997 20243 50243\nVmPeak: 2657056 kB\nVmSize: 2301416 kB\nVmLck: 0 kB\nVmPin: 0 kB\nVmHWM: 409676 kB\nVmRSS: 236604 kB\nRssAnon: 128356 kB\nRssFile: 103380 kB\nRssShmem: 4868 kB\nVmData: 1470524 kB\nVmStk: 8192 kB\nVmExe: 20 kB\nVmLib: 148356 kB\nVmPTE: 1936 kB\nVmPMD: 16 kB\nVmSwap: 4052 kB\nThreads: 149\nSigQ: 0/13364\nSigPnd: 0000000000000000\nShdPnd: 0000000000000000\nSigBlk: 0000000080001204\nSigIgn: 0000000000000001\nSigCgt: 0000000e400084f8\nCapInh: 0000000000000000\nCapPrm: 0000000000000000\nCapEff: 0000000000000000\nCapBnd: 0000000000000000\nCapAmb: 0000000000000000\nSeccomp: 2\nSpeculation_Store_Bypass: unknown\nCpus_allowed: 30\nCpus_allowed_list: 4-5\nMems_allowed: 1\nMems_allowed_list: 0\nvoluntary_ctxt_switches: 123554\nnonvoluntary_ctxt_switches: 3887\n```\n\n# 线程优化\n要优化线程数和线程栈,我们只能避免创建多余的线程,在合适的时机去结束空闲线程,来达到优化目的。接下来列出几个优化策略。\n### 1.提供线程池管理库\n为App提供线程池管理SDK,目的很简单,能避免业务组各自作战,从底层限制线程使用浪费。在SDK中我们可以提供IO,密集计算,单线程等API,方便上层调用同时,也能限制线程数。这里需要注意的是,我们要自定义实现一个ThreadFactory,用于为每个线程重命名,如以下代码:\n```\npublic class VThreadFactory implements ThreadFactory {\n private AtomicInteger mThreadNumber = new AtomicInteger(1);\n\n @Override\n public Thread newThread(@NonNull final Runnable r) {\n Runnable wrapperRunnable;\n if (mThreadPriority == Process.THREAD_PRIORITY_DEFAULT) {\n wrapperRunnable = r;\n } else {\n wrapperRunnable = new Runnable() {\n @Override\n public void run() {\n try {\n Process.setThreadPriority(mThreadPriority);\n } catch (Throwable ignore){}\n r.run();\n }\n };\n }\n Thread t = new Thread(group, wrapperRunnable,\n String.format(\"%s-%s-thread\", mPrefix, mThreadNumber.getAndIncrement()));\n if (t.isDaemon()) {\n t.setDaemon(false);\n }\n return t;\n }\n} \n```\n除了为每个线程重命名,我们还要为线程池调用以下API:\n```\n /**\n * Sets the policy governing whether core threads may time out and\n * terminate if no tasks arrive within the keep-alive time, being\n * replaced if needed when new tasks arrive. When false, core\n * threads are never terminated due to lack of incoming\n * tasks. When true, the same keep-alive policy applying to\n * non-core threads applies also to core threads. To avoid\n * continual thread replacement, the keep-alive time must be\n * greater than zero when setting {@code true}. This method\n * should in general be called before the pool is actively used.\n *\n * @param value {@code true} if should time out, else {@code false}\n * @throws IllegalArgumentException if value is {@code true}\n * and the current keep-alive time is not greater than zero\n *\n * @since 1.6\n */\n public void allowCoreThreadTimeOut(boolean value) {\n if (value && keepAliveTime <= 0)\n throw new IllegalArgumentException(\"Core threads must have nonzero keep alive times\");\n if (value != allowCoreThreadTimeOut) {\n allowCoreThreadTimeOut = value;\n if (value)\n interruptIdleWorkers();\n }\n }\n```\n含义为在time out时,能同时结束core threads。\n这里建议线程池管理SDK提供的池程数不要超过100,为其他三方SDK和系统留至少三分之二的线程额度。\n\n### 2.少用HandlerThread\nHandlerThread是Android提供的和Looper绑定的Thread辅助类,借助HandlerThread我们能在子线程中处理消息。但在实际使用过程,我们经常用静态类来避免内存泄漏,例如以下调用:\n```\nfinal static HandlerThread handlerThread;\nfinal static Handler handler;\nstatic {\n handlerThread = new HandlerThread(\"Bundle-thread\");\n handlerThread.start();\n handler = new Handler(handlerThread.getLooper());\n}\n```\n这种使用就会造成HandlerThread一直存在于内存中,thread实例不会结束。如果HandlerThread实例中还存在嵌套其他Thread或Stack内存相关函数,那就更不合理了。\n### 3.优化常见的池程池\n常见池程池的使用,比如AsyncTask类中用THREAD_POOL_EXECUTOR来执行异步操作,我们可以在Appliction或其他初始化代码块中执行以下代码:\n```\ntry {\n final ThreadPoolExecutor executor = ((ThreadPoolExecutor) android.os.AsyncTask.THREAD_POOL_EXECUTOR);\n executor.allowCoreThreadTimeOut(true);\n} catch (final Throwable t) {\n Log.e(\"OptAsyncTask\", \"Optimize AsyncTask executor error: allowCoreThreadTimeOut = true\", t);\n}\n```\n\n如果团队用的Okhttp作为网络基础库,尽量所有网络请求用同个HttpClient实例。Okhttp在Dispatcher、ConnectionPool等类中,用到了类实例或static线程池等。\n### 4.为每个线程命名\n如果团队在经过一系列优化后,还是避免不了pthread OOM异常,那我们就要从异常Log中能快速定位到问题代码。默认新建线程和Executors类中命名线程相关代码如下:\n```\n /**\n * Allocates a new {@code Thread} object. This constructor has the same\n * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}\n * {@code (null, null, gname)}, where {@code gname} is a newly generated\n * name. Automatically generated names are of the form\n * {@code \"Thread-\"+}<i>n</i>, where <i>n</i> is an integer.\n */\n public Thread() {\n init(null, null, \"Thread-\" + nextThreadNum(), 0);\n }\n```\n```\nDefaultThreadFactory() {\n SecurityManager s = System.getSecurityManager();\n group = (s != null) ? s.getThreadGroup() :\n Thread.currentThread().getThreadGroup();\n namePrefix = \"pool-\" +\n poolNumber.getAndIncrement() +\n \"-thread-\";\n}\n```\n如何我们要快速定位问题线程,采用默认方式肯定不行,所以我们要修改字节码,重新为每个线程命名,在新名字,带上使用类或其他有用信息。\n滴滴团队估计也面临了相同问题,在开源[booster](https://github.com/didi/booster)中利用ASM对Java字节码修改,实现了上述我们要为每个线程命名的需求。目前为止,booster在 [ThreadTransformer](https://github.com/didi/booster/blob/master/booster-transform-thread/src/main/kotlin/com/didiglobal/booster/transform/thread/ThreadTransformer.kt) 存在修改节码缺陷,以及[ShadowExecutors.newOptimizedFixedThreadPool](https://github.com/didi/booster/blob/aa3f74eedb70a47cd657e1dfc23361ffed988aa4/booster-android-instrument-thread/src/main/java/com/didiglobal/booster/instrument/ShadowExecutors.java)方法中错误的使用了LinkedBlockingQueue队列等缺陷,建议大家如果采用booster开源实现,尽量多作一些测试以及代码修复。\n# 结束语\n当APP功能逐渐庞大时,带来的不仅是包的大小,也同时带来了各种性能问题。pthread oom没有根治办法,我们只能减少发生量,尽量在性能测试中,提前发现问题,推动问题修复。\n\n# 参考\n1.android java process stack OOM:[https://blog.csdn.net/kongxinsun/article/details/78679860](https://blog.csdn.net/kongxinsun/article/details/78679860) \n2.dumpsys:[https://developer.android.com/studio/command-line/dumpsys](https://developer.android.com/studio/command-line/dumpsys) \n3.booster:[https://github.com/didi/booster](https://github.com/didi/booster)\n","source":"_posts/治理令人头痛的pthread-create-OutOfMemoryError错误.md","raw":"---\ntitle: 治理令人头痛的pthread_create OutOfMemoryError错误\ndate: 2019-09-20 17:44:10\ncategories: \n - Android\ntags: \n - 应用性能\n---\n# 引言\n我相信很多团队都面对过令人头痛的pthread_create 创建线程内存溢出问题。在Android中,典型的pthread_create内存溢出堆栈信息如下:\n\n```\n//此异常多为栈内存分配失败\njava.lang.OutOfMemoryError\npthread_create (1040KB stack) failed: Try again\n1 java.lang.Thread.nativeCreate(Native Method)\n2 java.lang.Thread.start(Thread.java:733)\n3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975)\n4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043)\n5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185)\n6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n7 java.lang.Thread.run(Thread.java:764)\n```\n\n```\n//此异常多为线程数到达上限\njava.lang.OutOfMemoryError\npthread_create (1040KB stack) failed: Out of memory\n1 java.lang.Thread.nativeCreate(Native Method)\n2 java.lang.Thread.start(Thread.java:743)\n3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)\n4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)\n5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)\n6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)\n7 java.lang.Thread.run(Thread.java:774)\n```\n\n\n出现创建线程内存溢出无非两个原因: \n1、进程的栈内存超过了虚拟机的最大内存数; \n2、线程数达到了系统最大限制数; \n关于线程数达到了系统最大限制数,在国内手机厂商中,华为手机在7.0+手机上已将最大线程数修改成了300。我们APP有大量的华为用户,不得不面对华为系统限制问题。 \nAndroid Dalvik和ART,将stack分为了java stack和native stack,本文没去具体实验,从[kongxinsun](https://blog.csdn.net/kongxinsun/article/details/78679860)的博客,我们了解到两者总量是1056KB。栈内存回收和堆内存策略不一样,比较简单,当线程结束,线程占用的栈内存也就回收了。 \n<!-- more -->\n接下来通过简述Android中常见的使用线程方式,尝试给出解决方案,优化线程引起的性能问题。\n# 线程使用\n在Android中,我们使用新建线程,无非就是想避免一些耗时操作,影响主线程响应。常见的相关类,比如Timer,ThreadHandle,AsyncTask,ThreadPoolExecutor等等都和线程直接相关。相关网络库、图片库、埋点库和其他三方SDK等都会存在大量使用线程场景。 \n查看当前所有激活的线程,我们可以通过以下API获取:\n```\n//输出总数\nFile file = new File(Environment.getExternalStorageDirectory(), \"threads.txt\");\nBufferedWriter writer = new BufferedWriter(new FileWriter(file, false));\nwriter.write(\"count:\" + Thread.getAllStackTraces().size() + \"\\n\");\nfor (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {\n writer.write(entry.getKey().getName() + \":\" + \"\\n\");\n for (StackTraceElement traceElement : entry.getValue()) {\n writer.write(\"\\tat \" + traceElement + \"\\n\");\n }\n}\n```\n\n```\n//输出内容\ncount:166\nConnectivityThread:\n at android.os.MessageQueue.nativePollOnce(Native Method)\n at android.os.MessageQueue.next(MessageQueue.java:336)\n at android.os.Looper.loop(Looper.java:174)\n at android.os.HandlerThread.run(HandlerThread.java:67)\npool-16-thread-1:\n at sun.misc.Unsafe.park(Native Method)\n at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)\n at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)\n at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)\n at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)\n at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)\n at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n at java.lang.Thread.run(Thread.java:919)\nBundleTaskExecutor #5:\n at sun.misc.Unsafe.park(Native Method)\n at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)\n at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:461)\n at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)\n at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:937)\n at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)\n at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)\n at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n at java.lang.Thread.run(Thread.java:919)\nexecute_task:\n at android.os.MessageQueue.nativePollOnce(Native Method)\n at android.os.MessageQueue.next(MessageQueue.java:336)\n at android.os.Looper.loop(Looper.java:174)\n at android.os.HandlerThread.run(HandlerThread.java:67)\nClassLoaderCreator #4:\n at sun.misc.Unsafe.park(Native Method)\n at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)\n at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2109)\n at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)\n at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)\n at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)\n at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)\n at java.lang.Thread.run(Thread.java:919)\n```\n这种方式我们能拿到Java层与其对接的native层thread总数,但是拿不到没有attach到java层的native thread,比如Futter engine中的native thread等。 \n如果为线程分配一个可定位到代码的名称,那我们完全能对症下药。但实际很难,我们没法约束开发同学和三方SDK为每个线程起自定义名称,例如上面的pool-16-thread-1的线程,我们很难定位到是哪个类发起的调用。 \n另外我们也可以直接dump进程内存,来查看内存情况,使用方式如下所示:\n```\nadb shell dumpsys meminfo [pacakgename]\n\nApplications Memory Usage (in Kilobytes):\nUptime: 122226380 Realtime: 451586500\n** MEMINFO in pid 19468 [pacakgename] **\n Pss Private Private SwapPss Heap Heap Heap\n Total Dirty Clean Dirty Size Alloc Free\n ------ ------ ------ ------ ------ ------ ------\n Native Heap 109473 109384 0 62 128460 122131 6328\n Dalvik Heap 17679 17604 0 63 26514 13257 13257\n Dalvik Other 5481 5480 0 0\n Stack 88 88 0 0\n Ashmem 326 124 0 0\n Gfx dev 55952 55952 0 0\n Other dev 216 0 216 0\n .so mmap 17532 1012 13224 21\n .jar mmap 2232 0 80 0\n .apk mmap 34716 132 19520 0\n .ttf mmap 199 0 140 0\n .dex mmap 45759 44872 492 0\n .oat mmap 213 0 80 0\n .art mmap 4888 4440 8 40\n Other mmap 9466 836 6200 0\n EGL mtrack 28572 28572 0 0\n GL mtrack 18996 18996 0 0\n Unknown 3572 3564 0 5\n TOTAL 355551 291056 39960 191 154974 135388 19585\n... \n```\n也可以查看线程等汇总数据,使用方式如下所示:\n```\nadb shell\nsargo:/ $ cat /proc/19468/status\nName: xxxx\nUmask: 0077\nState: S (sleeping)\nTgid: 19468\nNgid: 0\nPid: 19468\nPPid: 789\nTracerPid: 0\nUid: 10243 10243 10243 10243\nGid: 10243 10243 10243 10243\nFDSize: 512\nGroups: 3001 3002 3003 9997 20243 50243\nVmPeak: 2657056 kB\nVmSize: 2301416 kB\nVmLck: 0 kB\nVmPin: 0 kB\nVmHWM: 409676 kB\nVmRSS: 236604 kB\nRssAnon: 128356 kB\nRssFile: 103380 kB\nRssShmem: 4868 kB\nVmData: 1470524 kB\nVmStk: 8192 kB\nVmExe: 20 kB\nVmLib: 148356 kB\nVmPTE: 1936 kB\nVmPMD: 16 kB\nVmSwap: 4052 kB\nThreads: 149\nSigQ: 0/13364\nSigPnd: 0000000000000000\nShdPnd: 0000000000000000\nSigBlk: 0000000080001204\nSigIgn: 0000000000000001\nSigCgt: 0000000e400084f8\nCapInh: 0000000000000000\nCapPrm: 0000000000000000\nCapEff: 0000000000000000\nCapBnd: 0000000000000000\nCapAmb: 0000000000000000\nSeccomp: 2\nSpeculation_Store_Bypass: unknown\nCpus_allowed: 30\nCpus_allowed_list: 4-5\nMems_allowed: 1\nMems_allowed_list: 0\nvoluntary_ctxt_switches: 123554\nnonvoluntary_ctxt_switches: 3887\n```\n\n# 线程优化\n要优化线程数和线程栈,我们只能避免创建多余的线程,在合适的时机去结束空闲线程,来达到优化目的。接下来列出几个优化策略。\n### 1.提供线程池管理库\n为App提供线程池管理SDK,目的很简单,能避免业务组各自作战,从底层限制线程使用浪费。在SDK中我们可以提供IO,密集计算,单线程等API,方便上层调用同时,也能限制线程数。这里需要注意的是,我们要自定义实现一个ThreadFactory,用于为每个线程重命名,如以下代码:\n```\npublic class VThreadFactory implements ThreadFactory {\n private AtomicInteger mThreadNumber = new AtomicInteger(1);\n\n @Override\n public Thread newThread(@NonNull final Runnable r) {\n Runnable wrapperRunnable;\n if (mThreadPriority == Process.THREAD_PRIORITY_DEFAULT) {\n wrapperRunnable = r;\n } else {\n wrapperRunnable = new Runnable() {\n @Override\n public void run() {\n try {\n Process.setThreadPriority(mThreadPriority);\n } catch (Throwable ignore){}\n r.run();\n }\n };\n }\n Thread t = new Thread(group, wrapperRunnable,\n String.format(\"%s-%s-thread\", mPrefix, mThreadNumber.getAndIncrement()));\n if (t.isDaemon()) {\n t.setDaemon(false);\n }\n return t;\n }\n} \n```\n除了为每个线程重命名,我们还要为线程池调用以下API:\n```\n /**\n * Sets the policy governing whether core threads may time out and\n * terminate if no tasks arrive within the keep-alive time, being\n * replaced if needed when new tasks arrive. When false, core\n * threads are never terminated due to lack of incoming\n * tasks. When true, the same keep-alive policy applying to\n * non-core threads applies also to core threads. To avoid\n * continual thread replacement, the keep-alive time must be\n * greater than zero when setting {@code true}. This method\n * should in general be called before the pool is actively used.\n *\n * @param value {@code true} if should time out, else {@code false}\n * @throws IllegalArgumentException if value is {@code true}\n * and the current keep-alive time is not greater than zero\n *\n * @since 1.6\n */\n public void allowCoreThreadTimeOut(boolean value) {\n if (value && keepAliveTime <= 0)\n throw new IllegalArgumentException(\"Core threads must have nonzero keep alive times\");\n if (value != allowCoreThreadTimeOut) {\n allowCoreThreadTimeOut = value;\n if (value)\n interruptIdleWorkers();\n }\n }\n```\n含义为在time out时,能同时结束core threads。\n这里建议线程池管理SDK提供的池程数不要超过100,为其他三方SDK和系统留至少三分之二的线程额度。\n\n### 2.少用HandlerThread\nHandlerThread是Android提供的和Looper绑定的Thread辅助类,借助HandlerThread我们能在子线程中处理消息。但在实际使用过程,我们经常用静态类来避免内存泄漏,例如以下调用:\n```\nfinal static HandlerThread handlerThread;\nfinal static Handler handler;\nstatic {\n handlerThread = new HandlerThread(\"Bundle-thread\");\n handlerThread.start();\n handler = new Handler(handlerThread.getLooper());\n}\n```\n这种使用就会造成HandlerThread一直存在于内存中,thread实例不会结束。如果HandlerThread实例中还存在嵌套其他Thread或Stack内存相关函数,那就更不合理了。\n### 3.优化常见的池程池\n常见池程池的使用,比如AsyncTask类中用THREAD_POOL_EXECUTOR来执行异步操作,我们可以在Appliction或其他初始化代码块中执行以下代码:\n```\ntry {\n final ThreadPoolExecutor executor = ((ThreadPoolExecutor) android.os.AsyncTask.THREAD_POOL_EXECUTOR);\n executor.allowCoreThreadTimeOut(true);\n} catch (final Throwable t) {\n Log.e(\"OptAsyncTask\", \"Optimize AsyncTask executor error: allowCoreThreadTimeOut = true\", t);\n}\n```\n\n如果团队用的Okhttp作为网络基础库,尽量所有网络请求用同个HttpClient实例。Okhttp在Dispatcher、ConnectionPool等类中,用到了类实例或static线程池等。\n### 4.为每个线程命名\n如果团队在经过一系列优化后,还是避免不了pthread OOM异常,那我们就要从异常Log中能快速定位到问题代码。默认新建线程和Executors类中命名线程相关代码如下:\n```\n /**\n * Allocates a new {@code Thread} object. This constructor has the same\n * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}\n * {@code (null, null, gname)}, where {@code gname} is a newly generated\n * name. Automatically generated names are of the form\n * {@code \"Thread-\"+}<i>n</i>, where <i>n</i> is an integer.\n */\n public Thread() {\n init(null, null, \"Thread-\" + nextThreadNum(), 0);\n }\n```\n```\nDefaultThreadFactory() {\n SecurityManager s = System.getSecurityManager();\n group = (s != null) ? s.getThreadGroup() :\n Thread.currentThread().getThreadGroup();\n namePrefix = \"pool-\" +\n poolNumber.getAndIncrement() +\n \"-thread-\";\n}\n```\n如何我们要快速定位问题线程,采用默认方式肯定不行,所以我们要修改字节码,重新为每个线程命名,在新名字,带上使用类或其他有用信息。\n滴滴团队估计也面临了相同问题,在开源[booster](https://github.com/didi/booster)中利用ASM对Java字节码修改,实现了上述我们要为每个线程命名的需求。目前为止,booster在 [ThreadTransformer](https://github.com/didi/booster/blob/master/booster-transform-thread/src/main/kotlin/com/didiglobal/booster/transform/thread/ThreadTransformer.kt) 存在修改节码缺陷,以及[ShadowExecutors.newOptimizedFixedThreadPool](https://github.com/didi/booster/blob/aa3f74eedb70a47cd657e1dfc23361ffed988aa4/booster-android-instrument-thread/src/main/java/com/didiglobal/booster/instrument/ShadowExecutors.java)方法中错误的使用了LinkedBlockingQueue队列等缺陷,建议大家如果采用booster开源实现,尽量多作一些测试以及代码修复。\n# 结束语\n当APP功能逐渐庞大时,带来的不仅是包的大小,也同时带来了各种性能问题。pthread oom没有根治办法,我们只能减少发生量,尽量在性能测试中,提前发现问题,推动问题修复。\n\n# 参考\n1.android java process stack OOM:[https://blog.csdn.net/kongxinsun/article/details/78679860](https://blog.csdn.net/kongxinsun/article/details/78679860) \n2.dumpsys:[https://developer.android.com/studio/command-line/dumpsys](https://developer.android.com/studio/command-line/dumpsys) \n3.booster:[https://github.com/didi/booster](https://github.com/didi/booster)\n","slug":"治理令人头痛的pthread-create-OutOfMemoryError错误","published":1,"updated":"2025-06-02T13:15:33.846Z","comments":1,"layout":"post","photos":[],"_id":"cmbf44n8b000ucate8pr15la0","content":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>我相信很多团队都面对过令人头痛的pthread_create 创建线程内存溢出问题。在Android中,典型的pthread_create内存溢出堆栈信息如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//此异常多为栈内存分配失败</span><br><span class=\"line\">java.lang.OutOfMemoryError</span><br><span class=\"line\">pthread_create (1040KB stack) failed: Try again</span><br><span class=\"line\">1 java.lang.Thread.nativeCreate(Native Method)</span><br><span class=\"line\">2 java.lang.Thread.start(Thread.java:733)</span><br><span class=\"line\">3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975)</span><br><span class=\"line\">4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043)</span><br><span class=\"line\">5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185)</span><br><span class=\"line\">6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\">7 java.lang.Thread.run(Thread.java:764)</span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//此异常多为线程数到达上限</span><br><span class=\"line\">java.lang.OutOfMemoryError</span><br><span class=\"line\">pthread_create (1040KB stack) failed: Out of memory</span><br><span class=\"line\">1 java.lang.Thread.nativeCreate(Native Method)</span><br><span class=\"line\">2 java.lang.Thread.start(Thread.java:743)</span><br><span class=\"line\">3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)</span><br><span class=\"line\">4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)</span><br><span class=\"line\">5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)</span><br><span class=\"line\">6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)</span><br><span class=\"line\">7 java.lang.Thread.run(Thread.java:774)</span><br></pre></td></tr></table></figure>\n\n\n<p>出现创建线程内存溢出无非两个原因:<br>1、进程的栈内存超过了虚拟机的最大内存数;<br>2、线程数达到了系统最大限制数;<br>关于线程数达到了系统最大限制数,在国内手机厂商中,华为手机在7.0+手机上已将最大线程数修改成了300。我们APP有大量的华为用户,不得不面对华为系统限制问题。<br>Android Dalvik和ART,将stack分为了java stack和native stack,本文没去具体实验,从<a href=\"https://blog.csdn.net/kongxinsun/article/details/78679860\">kongxinsun</a>的博客,我们了解到两者总量是1056KB。栈内存回收和堆内存策略不一样,比较简单,当线程结束,线程占用的栈内存也就回收了。 </p>\n<span id=\"more\"></span>\n<p>接下来通过简述Android中常见的使用线程方式,尝试给出解决方案,优化线程引起的性能问题。</p>\n<h1 id=\"线程使用\"><a href=\"#线程使用\" class=\"headerlink\" title=\"线程使用\"></a>线程使用</h1><p>在Android中,我们使用新建线程,无非就是想避免一些耗时操作,影响主线程响应。常见的相关类,比如Timer,ThreadHandle,AsyncTask,ThreadPoolExecutor等等都和线程直接相关。相关网络库、图片库、埋点库和其他三方SDK等都会存在大量使用线程场景。<br>查看当前所有激活的线程,我们可以通过以下API获取:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//输出总数</span><br><span class=\"line\">File file = new File(Environment.getExternalStorageDirectory(), "threads.txt");</span><br><span class=\"line\">BufferedWriter writer = new BufferedWriter(new FileWriter(file, false));</span><br><span class=\"line\">writer.write("count:" + Thread.getAllStackTraces().size() + "\\n");</span><br><span class=\"line\">for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {</span><br><span class=\"line\"> writer.write(entry.getKey().getName() + ":" + "\\n");</span><br><span class=\"line\"> for (StackTraceElement traceElement : entry.getValue()) {</span><br><span class=\"line\"> writer.write("\\tat " + traceElement + "\\n");</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//输出内容</span><br><span class=\"line\">count:166</span><br><span class=\"line\">ConnectivityThread:</span><br><span class=\"line\"> at android.os.MessageQueue.nativePollOnce(Native Method)</span><br><span class=\"line\"> at android.os.MessageQueue.next(MessageQueue.java:336)</span><br><span class=\"line\"> at android.os.Looper.loop(Looper.java:174)</span><br><span class=\"line\"> at android.os.HandlerThread.run(HandlerThread.java:67)</span><br><span class=\"line\">pool-16-thread-1:</span><br><span class=\"line\"> at sun.misc.Unsafe.park(Native Method)</span><br><span class=\"line\"> at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)</span><br><span class=\"line\"> at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)</span><br><span class=\"line\"> at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\"> at java.lang.Thread.run(Thread.java:919)</span><br><span class=\"line\">BundleTaskExecutor #5:</span><br><span class=\"line\"> at sun.misc.Unsafe.park(Native Method)</span><br><span class=\"line\"> at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)</span><br><span class=\"line\"> at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:461)</span><br><span class=\"line\"> at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)</span><br><span class=\"line\"> at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:937)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\"> at java.lang.Thread.run(Thread.java:919)</span><br><span class=\"line\">execute_task:</span><br><span class=\"line\"> at android.os.MessageQueue.nativePollOnce(Native Method)</span><br><span class=\"line\"> at android.os.MessageQueue.next(MessageQueue.java:336)</span><br><span class=\"line\"> at android.os.Looper.loop(Looper.java:174)</span><br><span class=\"line\"> at android.os.HandlerThread.run(HandlerThread.java:67)</span><br><span class=\"line\">ClassLoaderCreator #4:</span><br><span class=\"line\"> at sun.misc.Unsafe.park(Native Method)</span><br><span class=\"line\"> at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)</span><br><span class=\"line\"> at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2109)</span><br><span class=\"line\"> at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\"> at java.lang.Thread.run(Thread.java:919)</span><br></pre></td></tr></table></figure>\n<p>这种方式我们能拿到Java层与其对接的native层thread总数,但是拿不到没有attach到java层的native thread,比如Futter engine中的native thread等。<br>如果为线程分配一个可定位到代码的名称,那我们完全能对症下药。但实际很难,我们没法约束开发同学和三方SDK为每个线程起自定义名称,例如上面的pool-16-thread-1的线程,我们很难定位到是哪个类发起的调用。<br>另外我们也可以直接dump进程内存,来查看内存情况,使用方式如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">adb shell dumpsys meminfo [pacakgename]</span><br><span class=\"line\"></span><br><span class=\"line\">Applications Memory Usage (in Kilobytes):</span><br><span class=\"line\">Uptime: 122226380 Realtime: 451586500</span><br><span class=\"line\">** MEMINFO in pid 19468 [pacakgename] **</span><br><span class=\"line\"> Pss Private Private SwapPss Heap Heap Heap</span><br><span class=\"line\"> Total Dirty Clean Dirty Size Alloc Free</span><br><span class=\"line\"> ------ ------ ------ ------ ------ ------ ------</span><br><span class=\"line\"> Native Heap 109473 109384 0 62 128460 122131 6328</span><br><span class=\"line\"> Dalvik Heap 17679 17604 0 63 26514 13257 13257</span><br><span class=\"line\"> Dalvik Other 5481 5480 0 0</span><br><span class=\"line\"> Stack 88 88 0 0</span><br><span class=\"line\"> Ashmem 326 124 0 0</span><br><span class=\"line\"> Gfx dev 55952 55952 0 0</span><br><span class=\"line\"> Other dev 216 0 216 0</span><br><span class=\"line\"> .so mmap 17532 1012 13224 21</span><br><span class=\"line\"> .jar mmap 2232 0 80 0</span><br><span class=\"line\"> .apk mmap 34716 132 19520 0</span><br><span class=\"line\"> .ttf mmap 199 0 140 0</span><br><span class=\"line\"> .dex mmap 45759 44872 492 0</span><br><span class=\"line\"> .oat mmap 213 0 80 0</span><br><span class=\"line\"> .art mmap 4888 4440 8 40</span><br><span class=\"line\"> Other mmap 9466 836 6200 0</span><br><span class=\"line\"> EGL mtrack 28572 28572 0 0</span><br><span class=\"line\"> GL mtrack 18996 18996 0 0</span><br><span class=\"line\"> Unknown 3572 3564 0 5</span><br><span class=\"line\"> TOTAL 355551 291056 39960 191 154974 135388 19585</span><br><span class=\"line\">... </span><br></pre></td></tr></table></figure>\n<p>也可以查看线程等汇总数据,使用方式如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">adb shell</span><br><span class=\"line\">sargo:/ $ cat /proc/19468/status</span><br><span class=\"line\">Name: xxxx</span><br><span class=\"line\">Umask: 0077</span><br><span class=\"line\">State: S (sleeping)</span><br><span class=\"line\">Tgid: 19468</span><br><span class=\"line\">Ngid: 0</span><br><span class=\"line\">Pid: 19468</span><br><span class=\"line\">PPid: 789</span><br><span class=\"line\">TracerPid: 0</span><br><span class=\"line\">Uid: 10243 10243 10243 10243</span><br><span class=\"line\">Gid: 10243 10243 10243 10243</span><br><span class=\"line\">FDSize: 512</span><br><span class=\"line\">Groups: 3001 3002 3003 9997 20243 50243</span><br><span class=\"line\">VmPeak: 2657056 kB</span><br><span class=\"line\">VmSize: 2301416 kB</span><br><span class=\"line\">VmLck: 0 kB</span><br><span class=\"line\">VmPin: 0 kB</span><br><span class=\"line\">VmHWM: 409676 kB</span><br><span class=\"line\">VmRSS: 236604 kB</span><br><span class=\"line\">RssAnon: 128356 kB</span><br><span class=\"line\">RssFile: 103380 kB</span><br><span class=\"line\">RssShmem: 4868 kB</span><br><span class=\"line\">VmData: 1470524 kB</span><br><span class=\"line\">VmStk: 8192 kB</span><br><span class=\"line\">VmExe: 20 kB</span><br><span class=\"line\">VmLib: 148356 kB</span><br><span class=\"line\">VmPTE: 1936 kB</span><br><span class=\"line\">VmPMD: 16 kB</span><br><span class=\"line\">VmSwap: 4052 kB</span><br><span class=\"line\">Threads: 149</span><br><span class=\"line\">SigQ: 0/13364</span><br><span class=\"line\">SigPnd: 0000000000000000</span><br><span class=\"line\">ShdPnd: 0000000000000000</span><br><span class=\"line\">SigBlk: 0000000080001204</span><br><span class=\"line\">SigIgn: 0000000000000001</span><br><span class=\"line\">SigCgt: 0000000e400084f8</span><br><span class=\"line\">CapInh: 0000000000000000</span><br><span class=\"line\">CapPrm: 0000000000000000</span><br><span class=\"line\">CapEff: 0000000000000000</span><br><span class=\"line\">CapBnd: 0000000000000000</span><br><span class=\"line\">CapAmb: 0000000000000000</span><br><span class=\"line\">Seccomp: 2</span><br><span class=\"line\">Speculation_Store_Bypass: unknown</span><br><span class=\"line\">Cpus_allowed: 30</span><br><span class=\"line\">Cpus_allowed_list: 4-5</span><br><span class=\"line\">Mems_allowed: 1</span><br><span class=\"line\">Mems_allowed_list: 0</span><br><span class=\"line\">voluntary_ctxt_switches: 123554</span><br><span class=\"line\">nonvoluntary_ctxt_switches: 3887</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"线程优化\"><a href=\"#线程优化\" class=\"headerlink\" title=\"线程优化\"></a>线程优化</h1><p>要优化线程数和线程栈,我们只能避免创建多余的线程,在合适的时机去结束空闲线程,来达到优化目的。接下来列出几个优化策略。</p>\n<h3 id=\"1-提供线程池管理库\"><a href=\"#1-提供线程池管理库\" class=\"headerlink\" title=\"1.提供线程池管理库\"></a>1.提供线程池管理库</h3><p>为App提供线程池管理SDK,目的很简单,能避免业务组各自作战,从底层限制线程使用浪费。在SDK中我们可以提供IO,密集计算,单线程等API,方便上层调用同时,也能限制线程数。这里需要注意的是,我们要自定义实现一个ThreadFactory,用于为每个线程重命名,如以下代码:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">public class VThreadFactory implements ThreadFactory {</span><br><span class=\"line\"> private AtomicInteger mThreadNumber = new AtomicInteger(1);</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> public Thread newThread(@NonNull final Runnable r) {</span><br><span class=\"line\"> Runnable wrapperRunnable;</span><br><span class=\"line\"> if (mThreadPriority == Process.THREAD_PRIORITY_DEFAULT) {</span><br><span class=\"line\"> wrapperRunnable = r;</span><br><span class=\"line\"> } else {</span><br><span class=\"line\"> wrapperRunnable = new Runnable() {</span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> public void run() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> Process.setThreadPriority(mThreadPriority);</span><br><span class=\"line\"> } catch (Throwable ignore){}</span><br><span class=\"line\"> r.run();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> };</span><br><span class=\"line\"> }</span><br><span class=\"line\"> Thread t = new Thread(group, wrapperRunnable,</span><br><span class=\"line\"> String.format("%s-%s-thread", mPrefix, mThreadNumber.getAndIncrement()));</span><br><span class=\"line\"> if (t.isDaemon()) {</span><br><span class=\"line\"> t.setDaemon(false);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return t;</span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n<p>除了为每个线程重命名,我们还要为线程池调用以下API:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"> /**</span><br><span class=\"line\"> * Sets the policy governing whether core threads may time out and</span><br><span class=\"line\"> * terminate if no tasks arrive within the keep-alive time, being</span><br><span class=\"line\"> * replaced if needed when new tasks arrive. When false, core</span><br><span class=\"line\"> * threads are never terminated due to lack of incoming</span><br><span class=\"line\"> * tasks. When true, the same keep-alive policy applying to</span><br><span class=\"line\"> * non-core threads applies also to core threads. To avoid</span><br><span class=\"line\"> * continual thread replacement, the keep-alive time must be</span><br><span class=\"line\"> * greater than zero when setting {@code true}. This method</span><br><span class=\"line\"> * should in general be called before the pool is actively used.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param value {@code true} if should time out, else {@code false}</span><br><span class=\"line\"> * @throws IllegalArgumentException if value is {@code true}</span><br><span class=\"line\"> * and the current keep-alive time is not greater than zero</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @since 1.6</span><br><span class=\"line\"> */</span><br><span class=\"line\"> public void allowCoreThreadTimeOut(boolean value) {</span><br><span class=\"line\"> if (value && keepAliveTime <= 0)</span><br><span class=\"line\"> throw new IllegalArgumentException("Core threads must have nonzero keep alive times");</span><br><span class=\"line\"> if (value != allowCoreThreadTimeOut) {</span><br><span class=\"line\"> allowCoreThreadTimeOut = value;</span><br><span class=\"line\"> if (value)</span><br><span class=\"line\"> interruptIdleWorkers();</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>含义为在time out时,能同时结束core threads。<br>这里建议线程池管理SDK提供的池程数不要超过100,为其他三方SDK和系统留至少三分之二的线程额度。</p>\n<h3 id=\"2-少用HandlerThread\"><a href=\"#2-少用HandlerThread\" class=\"headerlink\" title=\"2.少用HandlerThread\"></a>2.少用HandlerThread</h3><p>HandlerThread是Android提供的和Looper绑定的Thread辅助类,借助HandlerThread我们能在子线程中处理消息。但在实际使用过程,我们经常用静态类来避免内存泄漏,例如以下调用:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">final static HandlerThread handlerThread;</span><br><span class=\"line\">final static Handler handler;</span><br><span class=\"line\">static {</span><br><span class=\"line\"> handlerThread = new HandlerThread("Bundle-thread");</span><br><span class=\"line\"> handlerThread.start();</span><br><span class=\"line\"> handler = new Handler(handlerThread.getLooper());</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>这种使用就会造成HandlerThread一直存在于内存中,thread实例不会结束。如果HandlerThread实例中还存在嵌套其他Thread或Stack内存相关函数,那就更不合理了。</p>\n<h3 id=\"3-优化常见的池程池\"><a href=\"#3-优化常见的池程池\" class=\"headerlink\" title=\"3.优化常见的池程池\"></a>3.优化常见的池程池</h3><p>常见池程池的使用,比如AsyncTask类中用THREAD_POOL_EXECUTOR来执行异步操作,我们可以在Appliction或其他初始化代码块中执行以下代码:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">try {</span><br><span class=\"line\"> final ThreadPoolExecutor executor = ((ThreadPoolExecutor) android.os.AsyncTask.THREAD_POOL_EXECUTOR);</span><br><span class=\"line\"> executor.allowCoreThreadTimeOut(true);</span><br><span class=\"line\">} catch (final Throwable t) {</span><br><span class=\"line\"> Log.e("OptAsyncTask", "Optimize AsyncTask executor error: allowCoreThreadTimeOut = true", t);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>如果团队用的Okhttp作为网络基础库,尽量所有网络请求用同个HttpClient实例。Okhttp在Dispatcher、ConnectionPool等类中,用到了类实例或static线程池等。</p>\n<h3 id=\"4-为每个线程命名\"><a href=\"#4-为每个线程命名\" class=\"headerlink\" title=\"4.为每个线程命名\"></a>4.为每个线程命名</h3><p>如果团队在经过一系列优化后,还是避免不了pthread OOM异常,那我们就要从异常Log中能快速定位到问题代码。默认新建线程和Executors类中命名线程相关代码如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">/**</span><br><span class=\"line\"> * Allocates a new {@code Thread} object. This constructor has the same</span><br><span class=\"line\"> * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}</span><br><span class=\"line\"> * {@code (null, null, gname)}, where {@code gname} is a newly generated</span><br><span class=\"line\"> * name. Automatically generated names are of the form</span><br><span class=\"line\"> * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public Thread() {</span><br><span class=\"line\"> init(null, null, "Thread-" + nextThreadNum(), 0);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">DefaultThreadFactory() {</span><br><span class=\"line\"> SecurityManager s = System.getSecurityManager();</span><br><span class=\"line\"> group = (s != null) ? s.getThreadGroup() :</span><br><span class=\"line\"> Thread.currentThread().getThreadGroup();</span><br><span class=\"line\"> namePrefix = "pool-" +</span><br><span class=\"line\"> poolNumber.getAndIncrement() +</span><br><span class=\"line\"> "-thread-";</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>如何我们要快速定位问题线程,采用默认方式肯定不行,所以我们要修改字节码,重新为每个线程命名,在新名字,带上使用类或其他有用信息。<br>滴滴团队估计也面临了相同问题,在开源<a href=\"https://github.com/didi/booster\">booster</a>中利用ASM对Java字节码修改,实现了上述我们要为每个线程命名的需求。目前为止,booster在 <a href=\"https://github.com/didi/booster/blob/master/booster-transform-thread/src/main/kotlin/com/didiglobal/booster/transform/thread/ThreadTransformer.kt\">ThreadTransformer</a> 存在修改节码缺陷,以及<a href=\"https://github.com/didi/booster/blob/aa3f74eedb70a47cd657e1dfc23361ffed988aa4/booster-android-instrument-thread/src/main/java/com/didiglobal/booster/instrument/ShadowExecutors.java\">ShadowExecutors.newOptimizedFixedThreadPool</a>方法中错误的使用了LinkedBlockingQueue队列等缺陷,建议大家如果采用booster开源实现,尽量多作一些测试以及代码修复。</p>\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>当APP功能逐渐庞大时,带来的不仅是包的大小,也同时带来了各种性能问题。pthread oom没有根治办法,我们只能减少发生量,尽量在性能测试中,提前发现问题,推动问题修复。</p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>1.android java process stack OOM:<a href=\"https://blog.csdn.net/kongxinsun/article/details/78679860\">https://blog.csdn.net/kongxinsun/article/details/78679860</a><br>2.dumpsys:<a href=\"https://developer.android.com/studio/command-line/dumpsys\">https://developer.android.com/studio/command-line/dumpsys</a><br>3.booster:<a href=\"https://github.com/didi/booster\">https://github.com/didi/booster</a></p>\n","excerpt":"<h1 id=\"引言\"><a href=\"#引言\" class=\"headerlink\" title=\"引言\"></a>引言</h1><p>我相信很多团队都面对过令人头痛的pthread_create 创建线程内存溢出问题。在Android中,典型的pthread_create内存溢出堆栈信息如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//此异常多为栈内存分配失败</span><br><span class=\"line\">java.lang.OutOfMemoryError</span><br><span class=\"line\">pthread_create (1040KB stack) failed: Try again</span><br><span class=\"line\">1 java.lang.Thread.nativeCreate(Native Method)</span><br><span class=\"line\">2 java.lang.Thread.start(Thread.java:733)</span><br><span class=\"line\">3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:975)</span><br><span class=\"line\">4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1043)</span><br><span class=\"line\">5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1185)</span><br><span class=\"line\">6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\">7 java.lang.Thread.run(Thread.java:764)</span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//此异常多为线程数到达上限</span><br><span class=\"line\">java.lang.OutOfMemoryError</span><br><span class=\"line\">pthread_create (1040KB stack) failed: Out of memory</span><br><span class=\"line\">1 java.lang.Thread.nativeCreate(Native Method)</span><br><span class=\"line\">2 java.lang.Thread.start(Thread.java:743)</span><br><span class=\"line\">3 java.util.concurrent.ThreadPoolExecutor.addWorker(ThreadPoolExecutor.java:941)</span><br><span class=\"line\">4 java.util.concurrent.ThreadPoolExecutor.processWorkerExit(ThreadPoolExecutor.java:1009)</span><br><span class=\"line\">5 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1151)</span><br><span class=\"line\">6 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)</span><br><span class=\"line\">7 java.lang.Thread.run(Thread.java:774)</span><br></pre></td></tr></table></figure>\n\n\n<p>出现创建线程内存溢出无非两个原因:<br>1、进程的栈内存超过了虚拟机的最大内存数;<br>2、线程数达到了系统最大限制数;<br>关于线程数达到了系统最大限制数,在国内手机厂商中,华为手机在7.0+手机上已将最大线程数修改成了300。我们APP有大量的华为用户,不得不面对华为系统限制问题。<br>Android Dalvik和ART,将stack分为了java stack和native stack,本文没去具体实验,从<a href=\"https://blog.csdn.net/kongxinsun/article/details/78679860\">kongxinsun</a>的博客,我们了解到两者总量是1056KB。栈内存回收和堆内存策略不一样,比较简单,当线程结束,线程占用的栈内存也就回收了。 </p>","more":"<p>接下来通过简述Android中常见的使用线程方式,尝试给出解决方案,优化线程引起的性能问题。</p>\n<h1 id=\"线程使用\"><a href=\"#线程使用\" class=\"headerlink\" title=\"线程使用\"></a>线程使用</h1><p>在Android中,我们使用新建线程,无非就是想避免一些耗时操作,影响主线程响应。常见的相关类,比如Timer,ThreadHandle,AsyncTask,ThreadPoolExecutor等等都和线程直接相关。相关网络库、图片库、埋点库和其他三方SDK等都会存在大量使用线程场景。<br>查看当前所有激活的线程,我们可以通过以下API获取:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//输出总数</span><br><span class=\"line\">File file = new File(Environment.getExternalStorageDirectory(), "threads.txt");</span><br><span class=\"line\">BufferedWriter writer = new BufferedWriter(new FileWriter(file, false));</span><br><span class=\"line\">writer.write("count:" + Thread.getAllStackTraces().size() + "\\n");</span><br><span class=\"line\">for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {</span><br><span class=\"line\"> writer.write(entry.getKey().getName() + ":" + "\\n");</span><br><span class=\"line\"> for (StackTraceElement traceElement : entry.getValue()) {</span><br><span class=\"line\"> writer.write("\\tat " + traceElement + "\\n");</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">//输出内容</span><br><span class=\"line\">count:166</span><br><span class=\"line\">ConnectivityThread:</span><br><span class=\"line\"> at android.os.MessageQueue.nativePollOnce(Native Method)</span><br><span class=\"line\"> at android.os.MessageQueue.next(MessageQueue.java:336)</span><br><span class=\"line\"> at android.os.Looper.loop(Looper.java:174)</span><br><span class=\"line\"> at android.os.HandlerThread.run(HandlerThread.java:67)</span><br><span class=\"line\">pool-16-thread-1:</span><br><span class=\"line\"> at sun.misc.Unsafe.park(Native Method)</span><br><span class=\"line\"> at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)</span><br><span class=\"line\"> at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2067)</span><br><span class=\"line\"> at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1092)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\"> at java.lang.Thread.run(Thread.java:919)</span><br><span class=\"line\">BundleTaskExecutor #5:</span><br><span class=\"line\"> at sun.misc.Unsafe.park(Native Method)</span><br><span class=\"line\"> at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)</span><br><span class=\"line\"> at java.util.concurrent.SynchronousQueue$TransferStack.awaitFulfill(SynchronousQueue.java:461)</span><br><span class=\"line\"> at java.util.concurrent.SynchronousQueue$TransferStack.transfer(SynchronousQueue.java:362)</span><br><span class=\"line\"> at java.util.concurrent.SynchronousQueue.poll(SynchronousQueue.java:937)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\"> at java.lang.Thread.run(Thread.java:919)</span><br><span class=\"line\">execute_task:</span><br><span class=\"line\"> at android.os.MessageQueue.nativePollOnce(Native Method)</span><br><span class=\"line\"> at android.os.MessageQueue.next(MessageQueue.java:336)</span><br><span class=\"line\"> at android.os.Looper.loop(Looper.java:174)</span><br><span class=\"line\"> at android.os.HandlerThread.run(HandlerThread.java:67)</span><br><span class=\"line\">ClassLoaderCreator #4:</span><br><span class=\"line\"> at sun.misc.Unsafe.park(Native Method)</span><br><span class=\"line\"> at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:230)</span><br><span class=\"line\"> at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2109)</span><br><span class=\"line\"> at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1091)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1152)</span><br><span class=\"line\"> at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)</span><br><span class=\"line\"> at java.lang.Thread.run(Thread.java:919)</span><br></pre></td></tr></table></figure>\n<p>这种方式我们能拿到Java层与其对接的native层thread总数,但是拿不到没有attach到java层的native thread,比如Futter engine中的native thread等。<br>如果为线程分配一个可定位到代码的名称,那我们完全能对症下药。但实际很难,我们没法约束开发同学和三方SDK为每个线程起自定义名称,例如上面的pool-16-thread-1的线程,我们很难定位到是哪个类发起的调用。<br>另外我们也可以直接dump进程内存,来查看内存情况,使用方式如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">adb shell dumpsys meminfo [pacakgename]</span><br><span class=\"line\"></span><br><span class=\"line\">Applications Memory Usage (in Kilobytes):</span><br><span class=\"line\">Uptime: 122226380 Realtime: 451586500</span><br><span class=\"line\">** MEMINFO in pid 19468 [pacakgename] **</span><br><span class=\"line\"> Pss Private Private SwapPss Heap Heap Heap</span><br><span class=\"line\"> Total Dirty Clean Dirty Size Alloc Free</span><br><span class=\"line\"> ------ ------ ------ ------ ------ ------ ------</span><br><span class=\"line\"> Native Heap 109473 109384 0 62 128460 122131 6328</span><br><span class=\"line\"> Dalvik Heap 17679 17604 0 63 26514 13257 13257</span><br><span class=\"line\"> Dalvik Other 5481 5480 0 0</span><br><span class=\"line\"> Stack 88 88 0 0</span><br><span class=\"line\"> Ashmem 326 124 0 0</span><br><span class=\"line\"> Gfx dev 55952 55952 0 0</span><br><span class=\"line\"> Other dev 216 0 216 0</span><br><span class=\"line\"> .so mmap 17532 1012 13224 21</span><br><span class=\"line\"> .jar mmap 2232 0 80 0</span><br><span class=\"line\"> .apk mmap 34716 132 19520 0</span><br><span class=\"line\"> .ttf mmap 199 0 140 0</span><br><span class=\"line\"> .dex mmap 45759 44872 492 0</span><br><span class=\"line\"> .oat mmap 213 0 80 0</span><br><span class=\"line\"> .art mmap 4888 4440 8 40</span><br><span class=\"line\"> Other mmap 9466 836 6200 0</span><br><span class=\"line\"> EGL mtrack 28572 28572 0 0</span><br><span class=\"line\"> GL mtrack 18996 18996 0 0</span><br><span class=\"line\"> Unknown 3572 3564 0 5</span><br><span class=\"line\"> TOTAL 355551 291056 39960 191 154974 135388 19585</span><br><span class=\"line\">... </span><br></pre></td></tr></table></figure>\n<p>也可以查看线程等汇总数据,使用方式如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br><span class=\"line\">28</span><br><span class=\"line\">29</span><br><span class=\"line\">30</span><br><span class=\"line\">31</span><br><span class=\"line\">32</span><br><span class=\"line\">33</span><br><span class=\"line\">34</span><br><span class=\"line\">35</span><br><span class=\"line\">36</span><br><span class=\"line\">37</span><br><span class=\"line\">38</span><br><span class=\"line\">39</span><br><span class=\"line\">40</span><br><span class=\"line\">41</span><br><span class=\"line\">42</span><br><span class=\"line\">43</span><br><span class=\"line\">44</span><br><span class=\"line\">45</span><br><span class=\"line\">46</span><br><span class=\"line\">47</span><br><span class=\"line\">48</span><br><span class=\"line\">49</span><br><span class=\"line\">50</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">adb shell</span><br><span class=\"line\">sargo:/ $ cat /proc/19468/status</span><br><span class=\"line\">Name: xxxx</span><br><span class=\"line\">Umask: 0077</span><br><span class=\"line\">State: S (sleeping)</span><br><span class=\"line\">Tgid: 19468</span><br><span class=\"line\">Ngid: 0</span><br><span class=\"line\">Pid: 19468</span><br><span class=\"line\">PPid: 789</span><br><span class=\"line\">TracerPid: 0</span><br><span class=\"line\">Uid: 10243 10243 10243 10243</span><br><span class=\"line\">Gid: 10243 10243 10243 10243</span><br><span class=\"line\">FDSize: 512</span><br><span class=\"line\">Groups: 3001 3002 3003 9997 20243 50243</span><br><span class=\"line\">VmPeak: 2657056 kB</span><br><span class=\"line\">VmSize: 2301416 kB</span><br><span class=\"line\">VmLck: 0 kB</span><br><span class=\"line\">VmPin: 0 kB</span><br><span class=\"line\">VmHWM: 409676 kB</span><br><span class=\"line\">VmRSS: 236604 kB</span><br><span class=\"line\">RssAnon: 128356 kB</span><br><span class=\"line\">RssFile: 103380 kB</span><br><span class=\"line\">RssShmem: 4868 kB</span><br><span class=\"line\">VmData: 1470524 kB</span><br><span class=\"line\">VmStk: 8192 kB</span><br><span class=\"line\">VmExe: 20 kB</span><br><span class=\"line\">VmLib: 148356 kB</span><br><span class=\"line\">VmPTE: 1936 kB</span><br><span class=\"line\">VmPMD: 16 kB</span><br><span class=\"line\">VmSwap: 4052 kB</span><br><span class=\"line\">Threads: 149</span><br><span class=\"line\">SigQ: 0/13364</span><br><span class=\"line\">SigPnd: 0000000000000000</span><br><span class=\"line\">ShdPnd: 0000000000000000</span><br><span class=\"line\">SigBlk: 0000000080001204</span><br><span class=\"line\">SigIgn: 0000000000000001</span><br><span class=\"line\">SigCgt: 0000000e400084f8</span><br><span class=\"line\">CapInh: 0000000000000000</span><br><span class=\"line\">CapPrm: 0000000000000000</span><br><span class=\"line\">CapEff: 0000000000000000</span><br><span class=\"line\">CapBnd: 0000000000000000</span><br><span class=\"line\">CapAmb: 0000000000000000</span><br><span class=\"line\">Seccomp: 2</span><br><span class=\"line\">Speculation_Store_Bypass: unknown</span><br><span class=\"line\">Cpus_allowed: 30</span><br><span class=\"line\">Cpus_allowed_list: 4-5</span><br><span class=\"line\">Mems_allowed: 1</span><br><span class=\"line\">Mems_allowed_list: 0</span><br><span class=\"line\">voluntary_ctxt_switches: 123554</span><br><span class=\"line\">nonvoluntary_ctxt_switches: 3887</span><br></pre></td></tr></table></figure>\n\n<h1 id=\"线程优化\"><a href=\"#线程优化\" class=\"headerlink\" title=\"线程优化\"></a>线程优化</h1><p>要优化线程数和线程栈,我们只能避免创建多余的线程,在合适的时机去结束空闲线程,来达到优化目的。接下来列出几个优化策略。</p>\n<h3 id=\"1-提供线程池管理库\"><a href=\"#1-提供线程池管理库\" class=\"headerlink\" title=\"1.提供线程池管理库\"></a>1.提供线程池管理库</h3><p>为App提供线程池管理SDK,目的很简单,能避免业务组各自作战,从底层限制线程使用浪费。在SDK中我们可以提供IO,密集计算,单线程等API,方便上层调用同时,也能限制线程数。这里需要注意的是,我们要自定义实现一个ThreadFactory,用于为每个线程重命名,如以下代码:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br><span class=\"line\">27</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">public class VThreadFactory implements ThreadFactory {</span><br><span class=\"line\"> private AtomicInteger mThreadNumber = new AtomicInteger(1);</span><br><span class=\"line\"></span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> public Thread newThread(@NonNull final Runnable r) {</span><br><span class=\"line\"> Runnable wrapperRunnable;</span><br><span class=\"line\"> if (mThreadPriority == Process.THREAD_PRIORITY_DEFAULT) {</span><br><span class=\"line\"> wrapperRunnable = r;</span><br><span class=\"line\"> } else {</span><br><span class=\"line\"> wrapperRunnable = new Runnable() {</span><br><span class=\"line\"> @Override</span><br><span class=\"line\"> public void run() {</span><br><span class=\"line\"> try {</span><br><span class=\"line\"> Process.setThreadPriority(mThreadPriority);</span><br><span class=\"line\"> } catch (Throwable ignore){}</span><br><span class=\"line\"> r.run();</span><br><span class=\"line\"> }</span><br><span class=\"line\"> };</span><br><span class=\"line\"> }</span><br><span class=\"line\"> Thread t = new Thread(group, wrapperRunnable,</span><br><span class=\"line\"> String.format("%s-%s-thread", mPrefix, mThreadNumber.getAndIncrement()));</span><br><span class=\"line\"> if (t.isDaemon()) {</span><br><span class=\"line\"> t.setDaemon(false);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> return t;</span><br><span class=\"line\"> }</span><br><span class=\"line\">} </span><br></pre></td></tr></table></figure>\n<p>除了为每个线程重命名,我们还要为线程池调用以下API:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br><span class=\"line\">11</span><br><span class=\"line\">12</span><br><span class=\"line\">13</span><br><span class=\"line\">14</span><br><span class=\"line\">15</span><br><span class=\"line\">16</span><br><span class=\"line\">17</span><br><span class=\"line\">18</span><br><span class=\"line\">19</span><br><span class=\"line\">20</span><br><span class=\"line\">21</span><br><span class=\"line\">22</span><br><span class=\"line\">23</span><br><span class=\"line\">24</span><br><span class=\"line\">25</span><br><span class=\"line\">26</span><br></pre></td><td class=\"code\"><pre><span class=\"line\"> /**</span><br><span class=\"line\"> * Sets the policy governing whether core threads may time out and</span><br><span class=\"line\"> * terminate if no tasks arrive within the keep-alive time, being</span><br><span class=\"line\"> * replaced if needed when new tasks arrive. When false, core</span><br><span class=\"line\"> * threads are never terminated due to lack of incoming</span><br><span class=\"line\"> * tasks. When true, the same keep-alive policy applying to</span><br><span class=\"line\"> * non-core threads applies also to core threads. To avoid</span><br><span class=\"line\"> * continual thread replacement, the keep-alive time must be</span><br><span class=\"line\"> * greater than zero when setting {@code true}. This method</span><br><span class=\"line\"> * should in general be called before the pool is actively used.</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @param value {@code true} if should time out, else {@code false}</span><br><span class=\"line\"> * @throws IllegalArgumentException if value is {@code true}</span><br><span class=\"line\"> * and the current keep-alive time is not greater than zero</span><br><span class=\"line\"> *</span><br><span class=\"line\"> * @since 1.6</span><br><span class=\"line\"> */</span><br><span class=\"line\"> public void allowCoreThreadTimeOut(boolean value) {</span><br><span class=\"line\"> if (value && keepAliveTime <= 0)</span><br><span class=\"line\"> throw new IllegalArgumentException("Core threads must have nonzero keep alive times");</span><br><span class=\"line\"> if (value != allowCoreThreadTimeOut) {</span><br><span class=\"line\"> allowCoreThreadTimeOut = value;</span><br><span class=\"line\"> if (value)</span><br><span class=\"line\"> interruptIdleWorkers();</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>含义为在time out时,能同时结束core threads。<br>这里建议线程池管理SDK提供的池程数不要超过100,为其他三方SDK和系统留至少三分之二的线程额度。</p>\n<h3 id=\"2-少用HandlerThread\"><a href=\"#2-少用HandlerThread\" class=\"headerlink\" title=\"2.少用HandlerThread\"></a>2.少用HandlerThread</h3><p>HandlerThread是Android提供的和Looper绑定的Thread辅助类,借助HandlerThread我们能在子线程中处理消息。但在实际使用过程,我们经常用静态类来避免内存泄漏,例如以下调用:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">final static HandlerThread handlerThread;</span><br><span class=\"line\">final static Handler handler;</span><br><span class=\"line\">static {</span><br><span class=\"line\"> handlerThread = new HandlerThread("Bundle-thread");</span><br><span class=\"line\"> handlerThread.start();</span><br><span class=\"line\"> handler = new Handler(handlerThread.getLooper());</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>这种使用就会造成HandlerThread一直存在于内存中,thread实例不会结束。如果HandlerThread实例中还存在嵌套其他Thread或Stack内存相关函数,那就更不合理了。</p>\n<h3 id=\"3-优化常见的池程池\"><a href=\"#3-优化常见的池程池\" class=\"headerlink\" title=\"3.优化常见的池程池\"></a>3.优化常见的池程池</h3><p>常见池程池的使用,比如AsyncTask类中用THREAD_POOL_EXECUTOR来执行异步操作,我们可以在Appliction或其他初始化代码块中执行以下代码:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">try {</span><br><span class=\"line\"> final ThreadPoolExecutor executor = ((ThreadPoolExecutor) android.os.AsyncTask.THREAD_POOL_EXECUTOR);</span><br><span class=\"line\"> executor.allowCoreThreadTimeOut(true);</span><br><span class=\"line\">} catch (final Throwable t) {</span><br><span class=\"line\"> Log.e("OptAsyncTask", "Optimize AsyncTask executor error: allowCoreThreadTimeOut = true", t);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n\n<p>如果团队用的Okhttp作为网络基础库,尽量所有网络请求用同个HttpClient实例。Okhttp在Dispatcher、ConnectionPool等类中,用到了类实例或static线程池等。</p>\n<h3 id=\"4-为每个线程命名\"><a href=\"#4-为每个线程命名\" class=\"headerlink\" title=\"4.为每个线程命名\"></a>4.为每个线程命名</h3><p>如果团队在经过一系列优化后,还是避免不了pthread OOM异常,那我们就要从异常Log中能快速定位到问题代码。默认新建线程和Executors类中命名线程相关代码如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br><span class=\"line\">9</span><br><span class=\"line\">10</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">/**</span><br><span class=\"line\"> * Allocates a new {@code Thread} object. This constructor has the same</span><br><span class=\"line\"> * effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}</span><br><span class=\"line\"> * {@code (null, null, gname)}, where {@code gname} is a newly generated</span><br><span class=\"line\"> * name. Automatically generated names are of the form</span><br><span class=\"line\"> * {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.</span><br><span class=\"line\"> */</span><br><span class=\"line\">public Thread() {</span><br><span class=\"line\"> init(null, null, "Thread-" + nextThreadNum(), 0);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"gutter\"><pre><span class=\"line\">1</span><br><span class=\"line\">2</span><br><span class=\"line\">3</span><br><span class=\"line\">4</span><br><span class=\"line\">5</span><br><span class=\"line\">6</span><br><span class=\"line\">7</span><br><span class=\"line\">8</span><br></pre></td><td class=\"code\"><pre><span class=\"line\">DefaultThreadFactory() {</span><br><span class=\"line\"> SecurityManager s = System.getSecurityManager();</span><br><span class=\"line\"> group = (s != null) ? s.getThreadGroup() :</span><br><span class=\"line\"> Thread.currentThread().getThreadGroup();</span><br><span class=\"line\"> namePrefix = "pool-" +</span><br><span class=\"line\"> poolNumber.getAndIncrement() +</span><br><span class=\"line\"> "-thread-";</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>如何我们要快速定位问题线程,采用默认方式肯定不行,所以我们要修改字节码,重新为每个线程命名,在新名字,带上使用类或其他有用信息。<br>滴滴团队估计也面临了相同问题,在开源<a href=\"https://github.com/didi/booster\">booster</a>中利用ASM对Java字节码修改,实现了上述我们要为每个线程命名的需求。目前为止,booster在 <a href=\"https://github.com/didi/booster/blob/master/booster-transform-thread/src/main/kotlin/com/didiglobal/booster/transform/thread/ThreadTransformer.kt\">ThreadTransformer</a> 存在修改节码缺陷,以及<a href=\"https://github.com/didi/booster/blob/aa3f74eedb70a47cd657e1dfc23361ffed988aa4/booster-android-instrument-thread/src/main/java/com/didiglobal/booster/instrument/ShadowExecutors.java\">ShadowExecutors.newOptimizedFixedThreadPool</a>方法中错误的使用了LinkedBlockingQueue队列等缺陷,建议大家如果采用booster开源实现,尽量多作一些测试以及代码修复。</p>\n<h1 id=\"结束语\"><a href=\"#结束语\" class=\"headerlink\" title=\"结束语\"></a>结束语</h1><p>当APP功能逐渐庞大时,带来的不仅是包的大小,也同时带来了各种性能问题。pthread oom没有根治办法,我们只能减少发生量,尽量在性能测试中,提前发现问题,推动问题修复。</p>\n<h1 id=\"参考\"><a href=\"#参考\" class=\"headerlink\" title=\"参考\"></a>参考</h1><p>1.android java process stack OOM:<a href=\"https://blog.csdn.net/kongxinsun/article/details/78679860\">https://blog.csdn.net/kongxinsun/article/details/78679860</a><br>2.dumpsys:<a href=\"https://developer.android.com/studio/command-line/dumpsys\">https://developer.android.com/studio/command-line/dumpsys</a><br>3.booster:<a href=\"https://github.com/didi/booster\">https://github.com/didi/booster</a></p>"}],"PostAsset":[],"PostCategory":[{"post_id":"cmbf44n870008catea0848knm","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n89000fcate895g9ind"},{"post_id":"cmbf44n830001cate3g5ofogl","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n89000icate7klqh10q"},{"post_id":"cmbf44n880009cate63cja1w5","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8a000lcate4k4p641g"},{"post_id":"cmbf44n89000dcatea0l46yoh","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8b000ocateelfs63zg"},{"post_id":"cmbf44n860003cate9mqo4x0d","category_id":"cmbf44n88000acate2577gtjh","_id":"cmbf44n8b000rcatecm00eyfg"},{"post_id":"cmbf44n89000ecate7iy10w9a","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8b000vcate4esvc4iu"},{"post_id":"cmbf44n89000hcatefhjg1dh4","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8b000xcateekuh92gv"},{"post_id":"cmbf44n8a000kcate93gt3c54","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8c000zcateephfgcxv"},{"post_id":"cmbf44n8a000ncate2adveb92","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8c0011cate0egef3s6"},{"post_id":"cmbf44n8b000qcategt6m150u","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8c0013cate1l9oebk7"},{"post_id":"cmbf44n8b000ucate8pr15la0","category_id":"cmbf44n860004cate7x3c4vaw","_id":"cmbf44n8c0015cate93e22f06"}],"PostTag":[{"post_id":"cmbf44n830001cate3g5ofogl","tag_id":"cmbf44n870005cated77y16b4","_id":"cmbf44n89000ccategvv2hvdu"},{"post_id":"cmbf44n870007catehlsohttr","tag_id":"cmbf44n88000bcate36cgc2x4","_id":"cmbf44n89000jcate2i4wcab2"},{"post_id":"cmbf44n870008catea0848knm","tag_id":"cmbf44n89000gcatehanocc8q","_id":"cmbf44n8b000pcate47q6evru"},{"post_id":"cmbf44n8a000ncate2adveb92","tag_id":"cmbf44n89000gcatehanocc8q","_id":"cmbf44n8b000tcate3mrccuud"},{"post_id":"cmbf44n880009cate63cja1w5","tag_id":"cmbf44n89000gcatehanocc8q","_id":"cmbf44n8b000wcate00nxask0"},{"post_id":"cmbf44n89000dcatea0l46yoh","tag_id":"cmbf44n8b000scatebg6zdn73","_id":"cmbf44n8c0010catea9yn4y51"},{"post_id":"cmbf44n89000ecate7iy10w9a","tag_id":"cmbf44n89000gcatehanocc8q","_id":"cmbf44n8c0014catehvwl5anw"},{"post_id":"cmbf44n89000hcatefhjg1dh4","tag_id":"cmbf44n8c0012cateeii85s9s","_id":"cmbf44n8c0017catecsov8ao1"},{"post_id":"cmbf44n8a000kcate93gt3c54","tag_id":"cmbf44n8c0012cateeii85s9s","_id":"cmbf44n8c0019cate5h8i4vl5"},{"post_id":"cmbf44n8b000qcategt6m150u","tag_id":"cmbf44n8c0012cateeii85s9s","_id":"cmbf44n8c001bcate5nnr5fm0"},{"post_id":"cmbf44n8b000ucate8pr15la0","tag_id":"cmbf44n8c001acateh1kk7224","_id":"cmbf44n8c001ccate5tt6dxgb"}],"Tag":[{"name":"渲染","_id":"cmbf44n870005cated77y16b4"},{"name":"rust","_id":"cmbf44n88000bcate36cgc2x4"},{"name":"小功能组","_id":"cmbf44n89000gcatehanocc8q"},{"name":"埋点系统","_id":"cmbf44n8b000scatebg6zdn73"},{"name":"插件化","_id":"cmbf44n8c0012cateeii85s9s"},{"name":"应用性能","_id":"cmbf44n8c001acateh1kk7224"}]}}