内容更多是自己对于 2023 年的一个记录,算不上总结,文采也不好。不过很多事情,如果你不记录,就慢慢消失了。希望每次我翻看这篇记录的时候,都会感慨 2023 年真是丰富多彩的一年:有难忘的瞬间、有低谷、有朋自远方来、有去看大山大川;也会感慨有些事情做得很糟糕,如此这般其实可以做得更好;也会责备自己的懒惰,明明知道怎么做是对的,却因为懒惰没有去坚持。
各位看官一笑而过即可~
今年由于公司有骑行活动比赛,持续一整年,再加上成都环城绿道去年贯通,今年一整年的活动就是骑行。年初在闲鱼收了一个美利达瑞克多 4000,准备今年好好骑车。年初的目标是一圈(97km)骑到 3h 内,挑战目标是 2h 50min,没想到最终都达到了(总共骑了 35 圈绿道,最快用时是 2h 47min),如愿以偿在公司内部比赛中拿了第一名。同时把媳妇也带进了坑,一起骑了几圈。
自身叠加的那些 Debuff:哮喘、鼻炎,并没有明显的改善;不过最值得开心的是年底由于肺炎+甲流,住了 4 天院,打了四天吊瓶之后,鼻炎导致的味觉消失也被治好了,同时血氧也恢复到了 96% 以上(之前 always 在 94 以下,给我焦虑得不行)。成都的空气质量真是一言难尽,连华西的医生都直摇头,这一点想移居成都的同学可以慎重考虑一下。
新的一年还是得 骑车+跑步 一起抓,报了个都江堰半马,希望能完赛。减肥依然是主旋律,管住嘴迈开腿很重要,睡前保持饥饿感~
工作方面,今年不算很顺利,做的东西很杂,很多东西也没有最终落地,自己总结了一下,还是因为缺乏规划能力和执行力。不过也算积累了很多知识和做事方法,调研了很多技术方案,与客户也合作完成了几个项目。另外公司内部卧虎藏龙,需要虚心向每一个人学习。
AI 今年算是整了个大新闻,不管是公司还是个人,工作流中都插入了 AI 相关的内容:利用 AI 提升工作效率、创造 AI 相关的工具。不过由于公司性质,AI 还没有产生那种颠覆性的影响,不过我们都知道这一天很快就会来临。
“一种新技术一旦开始流行,你要么坐上压路机,要么成为铺路石。 —— Stewart Brand”
今年最重要的一次社交可能就是从成都去深圳参加 深圳 GDG DevFest 技术嘉年华,见到了几位老朋友和各位大佬,吃到了心心念念的砂锅粥和牛肉火锅,跟群里的小伙伴面了基,听了各位 GDE 的主题演讲,可谓是收获满满。
剩下的时间就是跟公司的几个小伙伴骑车了,能让大家周五晚上 12 点还在绿道上疯狂拉扯的,只有骑完之后那一顿火锅/麻辣烫了。@James @Hanzo @天宇 @宣总 @王 @陈大 @祥大 @果哥 @鹏哥 新的一年继续拉扯~
@福滕 和 @小陈 有了小宝宝,@周岩 的新房居然提前交房了(不过依然没有女朋友,狗头~)~,@老崔 跟 @和泉 都在深圳安家了。
交流最多的还是跟微信群里的小伙伴们,基本都是通过微信公众号或者博客添加的,基本都是 Android App 开发者或者国内的系统开发者,5 个群大概 2000 多人吧(虽然 90% 都是资深潜水员)。通过微信群认识了很多业内的大佬,手机圈本身就小,多个朋友多个家(狗头~),同时也感叹真是山外有山,人外有人,啥都别说了学就是了。
最大的感受是:最好跟在同一个频道上的人沟通,会减少很多沟通的成本。
小宝宝橘橘快三岁了,橘橘妈也换了新的组(跟我做相同的工作:系统性能优化),一家人开开心心平平安安,就是最大的心愿了。今年由于各种原因,在家里陪宝宝和家人的时间有点少,出游计划也多被搁置。新的一年要提升效率,把时间多用在陪伴家人。
今天读到一个博主的 2023 年终总结,其中一段话让我感触很深,这才是生活的最终目标,不要本末倒置:
我希望多丰富一些体验,做一些没做过的事情,看一些没看过的风景。我希望 有一天可以把时间用在自己身上,而不是用在工作上,不是在那些不重要的人,或者事身上。更具体一点,我希望家人身体健康、平平安安,有爱,有陪伴,我也希望有一天可以走遍中国、走遍世界,看那些没看过的风景,吃那些没吃过的美食。
当我想到这里,我豁然开朗,我知道我终其一生追求的是什么,剩余的,都不重要的。那么,我怎样才可以?我需要时间,我需要钱。
从此开始,我所有的事情都围绕着这个目的展开,只要某件事情,对我的人生最终目的是有推进作用,无论多么艰难,我都必须做,是的,必须,没有任何商量的余地。
今年学习方面一败涂地,英语学习基本上停滞,最后这个月才重新拾起来;技术学习更多停留在读,写和思考总结都比较少;文化学习,比如阅读,书架上的书很多,读完的很少;博客也只更新了几篇文章,费曼学习法那种输出倒逼输入,道理都懂,还是没有克服拖延症。
工作中倒是养成了一个习惯:在公司内部使用 Typora 记录每天的工作内容、技术专项、技术调研、Bug 记录等;新的一年切换到 Loop 之后,我会在上面更详细地记录和思考,以及组内分享,带动组内的技术氛围(今年年初吹下的牛皮年底了最终还是没有实现)。
从我自己的学习经历来看,学习有三大障碍:
年初看了李自然的一个视频:人生如逆旅,我亦是行人,视频主要是讲了下面几个点,当时感触很深,也分享给大家:【李自然说】人生如逆旅,我亦是行人
今年去了四个地方:四姑娘山,厦门,烟台+威海,乐山。
带着宝宝出去玩虽然很折腾,但是还是很值的,宝宝在一路上会认识很多新的东西,交新的朋友,吃到新的食物。2024 依然会继续带着宝宝折腾~
终于来到轻松一点的环节了,今年主要兴趣是骑车,购置了一台大疆 Action4 来拍骑车时候的视频,要技术没有,都是一镜到底,创造力这玩意是真得有天赋才行。
自我感觉良好,除了稍微有一些焦虑。自我感觉良好很大程度上得益于家庭的支持,有爸妈在家帮忙看宝宝,我和媳妇双 it,很多决定媳妇都会坚定支持。成都美食比较多,比较杂,想吃哪种都可以吃到,玩的地方也比较多,川西、古镇、都江堰青城山,去重庆也不远;天府新区高新区这边也不怎么排外,呆着很舒服(除了雾霾,这个真的是负分)
2024 继续保持吧,心态要好,睡眠要好。
房贷还剩余比较多,每个月工资大半都交给银行了,除了夫妻两个的工资基本没有其他的收入,这也是我目前比较焦虑的点,一旦遇到被辞退或者其他变故,周转就会有困难。所以最终目标还是要管好自己的钱袋子,控制花销。看了一个说法,分别是穷人、中产阶级、富人的资产负债情况:
所以最终还是要持有 优质资产,我理解这里的优质资产可以是房产,也可以是知识。结合个人的情况,我思索再三,又重新开启了 知识星球,简介在这里 The Performance 知识星球简介,我并没有做宣传,只在博客中有介绍。我觉得目前最有效的两种学习方法:费曼学习法和公开学习法,知识星球都可以满足我,我会把日常的一些思考、案例、学习的进度、工具等分享到星球上;如果在这个过程中,有些知识能帮助到你,那么我觉得就值了。
今年可能最值得拿出来说的就是去了 电子科技大学 ,给研究生们上了三天的课,蹭的当然是公司跟企业合作这个便利,不过过程还是很值得一说的:从接到这个任务,到开始备课(其实就是学习 @鹏哥 去年准备的 PPT),再到每个知识点的学习,再到真正去讲课,还是充满了挑战的,毕竟是第一次,还好没有翻车。
今年好像还是我,这次就没那么紧张了,不过去年的内容要好好更新一下,讲课的方式需要再优化优化,知识点该深入的还是要继续深入,毕竟 Android 从最上层到最底层都讲清楚也不是那么容易的.
工作方面的就不说了,除了工作方面,最大的挑战应该还是英语口语了,今年有好几次需要用到英语口语的时候,结果都不是那么好,这也刺激我要好好学英语。这个感觉要放到 2024 年最重要的 Task 里面,目前也有一些小的规划在实施中。
文笔有限,借用 aoxiang 的话来做个结尾吧:
]]>很感谢对我的认可,你花时间看到这里,我很希望这篇文章对你有价值。同时,我也很希望你能想清楚「你想要过什么样的人生」,当你有了这个判定标准,你任何一个抉择,都会无比轻松。
随着 Android 的发展,性能优化成了所有 Android 手机厂商和 App 厂商的重中之重。然而性能优化又是一个非常宽泛的话题,涉及到的知识非常多,从底层 Kernel 到 Framework 再到 App,每一个环节都有大量的知识点需要了解。所以笔者团队建立了这个专门分享和讨论 Android Performance 相关的技术圈:提供高质量的技术分享和技术讨论,提供系统性的 Android 性能优化相关知识的学习。
所有加入星球的同学,都可以畅所欲言,分享知识和案例,提问或者解答他人的问题,以问题分析带动学习,共同学习,共同进步,所有人都可以分享和讨论下面话题相关的内容,或者享受相关的权益。
目前星球的内容规划如下(两个 ## 之间的是标签,相关的话题都会打上对应的标签,方便大家点击感兴趣的标签查看对应的知识),由于改为个人经营,精力有限,划线部分是暂时删除。
微信扫码加入即可(iOS 用户最好用微信扫码)
当然星球还有一个免费的版本,定位是知识分享,感兴趣的也可以加入
当然也欢迎关注微信公众号~
新的微信交流群,没有加之前微信群都可以加这个,方便平时随时沟通和大家相互之间讨论
微信群旨在讨论 Android 及其相关的技术话题,包括但不限于 Android 性能优化课题(响应速度、流畅度、ANR、Crash、内存、耗电、性能监控等)Android App 开发、Framework 开发、Linux、大前端、面试分享、技术招聘等话题
鉴于群里的各位大佬们时间都很宝贵,大家聊天的内容尽量与上面的主题相关,禁止吹水、装机、购机、手机厂商优劣讨论……否则群会被贴上水群的标签,希望大家共同维护群氛围
另外还有 闲聊吹水群、跑步群、读书群、GPT 群,里面大家就随意发挥了,可以私我拉进去
如果群满 200 或者二维码失效,可以加我微信拉各位进去(553000664)
I am embarking on a new series of articles addressing various considerations in OS architecture design. Indeed, these considerations are not limited to OS but are applicable to the design of any large-scale software.
I am limited by my capabilities and knowledge and bring a highly subjective view, and there are undoubtedly inadequacies. I am eager to hear different thoughts and perspectives, and through the collision of ideas, we can achieve a deeper understanding.
In my opinion, the core differences between Android and iOS from the OS perspective are primarily manifested in:
Why do they adopt different strategic decisions? It relates to the factors considered during architectural design. A software architectural decision is a selection of the most suitable decision for the present and foreseeable future amidst a series of current considerations; it is a collection of decisions. Thus, there’s no absolute right or wrong in architectural design, or rather, rational or irrational. Different projects and decision-makers face different considerations and prioritize different aspects. If architects of similar skill levels switch scenarios, they are likely to make similar decisions. This indicates that architectural design is an engineering process and a technical craft, learnable and following certain patterns.
The challenge in architectural design lies in accurately understanding the environment the organization operates within, current and foreseeable considerations, and finding the most suitable methodologies or tech stacks from existing engineering practices. It is evident that considerations play a significant role. What factors need to be considered in software architectural design? These include, but are not limited to, testability, component release efficiency, development efficiency, security, reliability, performance, scalability, etc. Experienced architects, especially those with operational experience in similar businesses, are better at discerning what to focus on at different times, what must be adhered to, and what can be relaxed or even abandoned. Considering the law of diminishing marginal returns, the consideration of influencing factors and decision-making behavior will permeate the entire lifecycle, introducing another art of decision-making.
We can summarize that:
Interestingly, one consideration may conflict with another, leading to a situation where one cannot have the best of both worlds. For example, improving component development efficiency might impact program performance. So, what were the considerations for the designers of Android and iOS in the context of mobile OS design? To answer this question, we first need to explain the relationship between the OS, application programs, and the kernel.
First, it should be noted that the OS is part of the software stack above the hardware. It, along with application programs, constructs the complete software program stack, utilizing the hardware capabilities to provide services to users. From the hardware’s perspective, regardless of the OS or application programs, both are software; however, the OS has higher privileges, allowing it to operate in the CPU’s high-level modes, for tasks such as direct interaction with hardware and executing interrupt handling routines. From the software developer’s perspective, however, the OS and application programs are entirely different entities. Application programs utilize the capabilities provided by the OS to meet their business requirements or use hardware capabilities for tasks like playing music or storing data.
At a higher level, from the user experience perspective, application programs, OS, and hardware are all part of the same entity. When issues arise in their collaboration, ordinary consumers might simply feel that the device is less than ideal. Therefore, all three parties are obligated to cooperate and facilitate each other; only when the consumer is well-served can these entities sustainably profit.
In discussing the OS, it’s crucial to recognize that the vast majority of modern OSes consist of a kernel and system services.
Current mainstream operating systems include macOS, Windows, and various derivatives of Linux. Clearly, Android belongs to the Linux derivatives, with another renowned version in the developer community being Ubuntu. Due to the complexity and path-dependent nature of a complete OS, derivatives are often deployed on different hardware and application scenarios. For a while, Linux was lauded for its extensive device radiation and wide application scenario spectrum. However, being able to run an OS and running it well—connecting hardware and application programs to offer an optimal user experience—are two different things.
An increasingly common understanding is that the OS’s mechanisms, policies, and closely associated application programs vary greatly depending on the application scenario.
For instance, even if based on the same Linux kernel, the use and system services built upon it for embedded devices, smartwatches, smartphones, large servers, or even smart cars are distinctly different, as are the operating strategies of application programs. The Linux kernel and device drivers can be seen as the bridge between hardware and system services—a standard bridge—but the vehicles and pedestrians traversing it are entirely distinct.
This leads to another classic OS design concept: mechanism and policy.
The “separation principle” noted in UNIX programming specifies the separation of policy from mechanism and interface from the engine. Mechanisms provide capabilities; policies dictate how those capabilities are used.
In this context, the memory management, process management, VFS layer, and network programming interfaces that Linux provides are mechanisms. Memory allocation and release mechanisms, process schedulers, frequency and core allocators, and different file systems are policies. Going further, identifying which processes interact with users and which are background tasks, synchronizing this information to the scheduler to determine the optimal process for the next scheduling window is fundamentally a policy built upon the Linux process management mechanism. Similarly, notifying future computational task requirements to the CPU frequency allocator for dynamic adjustment (DVFS) involves a policy and a mechanism.
For instance, all modern OSes support memory compression capabilities, but different OSes use this mechanism according to their own characteristics to best meet business needs. iOS exhibits process-level compression, while Android relies on Linux’s ZRAM.
Though OS mechanisms might be similar, policies are diverse. The extent of their differences depends on the OS designers’ understanding of their own business—meaning the application programs running on the OS and the kind of experience and services they aim to provide users. Early Android was essentially a desktop architecture, but modifications, especially by domestic manufacturers, have made it increasingly resemble iOS, aligning more with the system capabilities required by mobile device operating systems.
The OS for smartphones and smart cars probably faces a similar scenario—they cannot be directly transplanted.
One interesting aspect of policy is that implementing one policy often brings up another issue, necessitating the introduction of an additional policy. When one policy compensates for another, a chain forms, eventually creating a closed-loop mechanism. In other words, all policies must be in effect simultaneously to maximize the system’s benefits. When learning about a competitor OS’s policies, remembering this aspect is essential; otherwise, we might only grasp the superficial aspects, leading to “negative optimization” once the features are launched.
Now, narrowing it down to Android and iOS, where do their strategy designs originate?
Apple has released a series of operating systems including macOS, iOS, iPadOS, watchOS, and tvOS. The distinctions between them are not merely in brand names but are characterized by specific strategy variations. While they largely share underlying mechanisms, their strategies are distinctly different. For instance, the background running mechanism on iOS is vastly different from that on macOS. iOS resembles a “restricted version of multitasking,” while macOS offers genuine multitasking. Hence, it’s not that iOS can’t implement multitasking but rather a deliberate design decision.
Android, as we refer to it, is actually a project open-sourced by Google, known as AOSP (Android Open Source Project). Device manufacturers adapt AOSP and integrate their services based on their business models and understanding of target users. Apple is singular, but there are numerous device manufacturers, each with their own profit models and interpretations of user needs. They modify AOSP accordingly, and the market decides which version prevails.
From a technical perspective, AOSP is rich in mechanisms but lacks in strategies. Google has implemented these strategies within its GMS services. Users outside mainland China, like those using Pixel or Samsung phones, would experience Google’s suite of services. Although the ecosystem is considered subpar in China due to the proliferation of substandard apps, the situation is somewhat mitigated overseas, but still not comparable to Apple’s ecosystem.
Given Google’s less-than-ideal strategic implementation, domestic manufacturers in China have carved out space for themselves. The intense competition and the sheer volume of phone shipments in mainland China have led manufacturers to prioritize consumer feedback and innovative adaptations of AOSP.
The most significant difference between iOS and Android stems from their respective strategies, rooted in their initial service objectives and developmental goals.
Books like “Steve Jobs” and “Becoming Steve Jobs” touch upon the development of the iPhone and the discussions around AppStore. Jobs was initially resistant to allowing third-party app development on mobile devices due to concerns about power consumption, performance, and security. The initial intent was to create a device that offered an unparalleled user experience, not necessarily catering to every user demand.
As Apple had written the first batch of apps themselves, they amassed a wealth of insights on designing excellent embedded device applications, leading to the creation of effective API systems. This comprehensive approach from hardware to software was not for the sake of exclusivity, but a necessary path to crafting the best user experience.
Contrastingly, during 2007-2008, Android was focused on getting the system up and running. Android’s initial aim was to accommodate a vast array of app developers, leading to its favoring of Java, a popular language among developers and in the embedded device domain. Although Android later shifted to Android Studio, improving the development experience, it still lagged behind Apple’s Xcode in terms of application development and debugging tools.
Apple’s strong control over its app ecosystem, partly attributed to its powerful IDE tools, aids developers in solving problems rather than imposing constraints. Further, initiatives like LLVM, Swift, and SwiftUI underscore Apple’s commitment to facilitating superior app development to enhance the user experience.
The purpose of designing an OS is profit-oriented, and it should facilitate app developers in crafting quality programs. Apple has showcased that offering quality developer services can be instrumental in achieving optimal device experiences. A summary of insights gleaned from Apple’s approach includes:
While Apple exercises absolute control, it also offers software services that are significantly above industry standards. Offering an OS is merely a means; understanding the nature of the relationship with developers and providing developer services, such as IDE, is a more profound consideration at the cognitive level.
The greatest feature of mobile devices is their portability, enabled by battery power. Besides, as handheld devices, they primarily rely on passive cooling since they don’t have an active cooling mechanism (exceptional cases of gaming phones and attachable fans aside). Currently, there are two trends: one, the transistor fabrication process is inching closer to its physical limit, and two, more functionalities are being integrated into a single chip. This increase in the number of active transistors (or their area) leads to a corresponding rise in heat emission, although it wasn’t a primary concern during the early days of smartphones. Now, the balance between power consumption and performance has become a significant challenge for smartphones.
More active threads mean the CPU remains busy, resulting in reduced CPU time slices allocated to user-related programs, thus impacting performance. Therefore, the design of mobile device OSes naturally leads to restrictions on resource utilization by applications. If left unrestricted like servers or desktop computers, it would be impossible to maintain a balance between performance, power consumption, and heat dissipation. The more constrained a device is in terms of performance and power consumption, the stricter the control over application programs, as is the case with smartwatches.
Both Android and iOS have their resource protection mechanisms. In Android, the most common is the OOM (Out Of Memory) mechanism. When the heap memory usage of a Java application exceeds a certain threshold, the system terminates it. Although Android has a mechanism to detect excessive CPU usage, it is somewhat rudimentary and only monitors the CPU usage of regular applications, not system or native thread (written in languages other than Java).
In contrast, iOS has a plethora of mechanisms ranging from CPU, memory, to even restrictions on excessive IO writes, including:
iOS outlines these behaviors in developer documentation to clarify the reasons for unexpected application exits.
Google’s lax approach to Android’s design has provided ample room for domestic manufacturers to introduce their overload protection strategies (similar to iOS’s, with minor variations) to ensure phones are not compromised by substandard applications. However, the issue lies in the lack of transparency about system termination behaviors. Developers are often in the dark about why their applications are terminated. Even if they are aware of the reasons, the lack of debugging information during termination impedes improvement efforts since no manufacturer releases this information.
Consequently, application developers resort to various “black technologies” to keep their applications alive and bypass the system’s detection mechanisms. What should have been a collaborative ecosystem building effort has turned into a battleground. In the end, both parties suffer, with consumers bearing the brunt of the damage.
In an ideal world:
Manufacturers and developers should be partners. Manufacturers may need to do more to assist developers, as many capabilities are exclusive to them. Blaming developers solely for poor quality is not a competitive approach for manufacturers.
The fault, in this case, is at the cognitive level.
Different device forms pursue varied user experience requirements, leading to diverse OS design necessities. In desktop OS, the lifecycle of an application is entirely under its control, aiming to maximize the program’s potential. This design is viable because desktop computers are not constrained by power consumption and heat dissipation and rarely face performance bottlenecks. Their primary concern is exploiting the machine’s capabilities to the fullest.
On the contrary, smartphones are a different story due to their limitations in power consumption and heat generation. Similarly, smartwatches also suffer from these restrictions but to a more stringent degree. No one desires a watch that heats up their wrist and cannot last a day on a full charge. Moreover, their performance and memory limitations mean that too many apps can’t remain active in the background, necessitating a centralized management module to uniformly implement services for most common applications, known as a hosted architecture. While smart cars aren’t constrained by performance, power, or heat, they require high stability. Unless completely powered down, core system services must remain operational, emphasizing the importance of system anti-aging design.
A core strategy in smartphone OS design revolves around lifecycle management, determining the entire journey of an application from its inception to termination. Android leans towards desktop system design, offering a “looser” strategy and more room for developers to maneuver. In contrast, iOS imposes more restrictions; an application relegated to the background only has about 5 seconds to perform background tasks before entering the Suspend state. In this state, the application is denied CPU scheduling, rendering it “quiet” when in the background.
Chinese manufacturers, after obtaining AOSP code, have replicated a mechanism similar to iOS’s Suspend. However, due to the lack of native support in AOSP, compromises were made, resulting in an implementation not as thorough as iOS. Android interprets this running strategy as the developers’ responsibility to create well-crafted applications – a notion I find naive and impractical. By this logic, human societal development would never have required laws, an idea that contradicts human nature. Fortunately, Google might have realized this issue, gradually enhancing the so-called “freezing” strategy in their annual updates, albeit less effective than improvements made by domestic manufacturers. The progress in AOSP is slow, and substantial changes in this area aren’t expected in the next two to three years.
So, if an application is Suspended in the background on iOS, how can it perform required background computations? iOS introduced the BackgroundTask mechanism, allowing applications to request permission for background task execution, with the system intelligently scheduling these tasks. Hence, iOS offers a strategy for application background operation but places the final decision in the system’s hands. This allows the system to schedule background tasks based on the phone’s current status, avoiding task execution during high system load periods to reduce overall load. The system also assigns daily quotas to each application, incorporating execution frequency and duration as crucial factors. Generally, tasks are allowed about 30 seconds of execution before being terminated by the system.
However, background tasks aren’t limited to computations. How are requirements like playing music or location tracking addressed? Applications needing these services must declare them explicitly in the IDE, with the App Store checking for a match between the application and requested permissions – a mismatch leads to rejection. The App Store is central to iOS’s lifecycle management mechanism, enabling quality control during the application’s listing and operational phases. Applications identified as subpar are flagged for the developers to fix, facing delisting otherwise. Post-Suspend, the system may also terminate applications as part of overload protection. The most common reason is memory reclamation, especially given the expense of memory chips; without opting for larger memory, terminating applications is the only way to free up more memory.
So, if the application isn’t even running, how are background tasks executed, and messages received? Thanks to BackgroundTask design, even if an application is terminated, the system will automatically restart it to execute background tasks when conditions are met. Message reception is achieved through notification mechanisms, with two kinds: one displaying detailed content in the notification bar, activating the application only upon user interaction; the other is for VoIP class applications, capable of actively restarting terminated applications.
Android possesses a similar mechanism but requires the integration of its GMS service. Due to uncertain reasons, this service is inaccessible in China, forcing domestic apps to rely on various “dark arts” and commercial collaborations to keep their programs alive in the background for message reception. This has led to a grotesque scenario where head applications, often used by users, are greenlit by manufacturers, who, upon realizing this trend, keep intensifying various services, treating the phone as their playground and squeezing every bit of system memory. Could manufacturers offer a notification service akin to this? They could, but the construction and operational costs are disproportionately high compared to their sales profits, leading to the only option of increasing memory capacity, passing the price pressure onto consumers. The overall cost of a complete machine has an upper limit; bolstering memory means cutting corners elsewhere. For domestic manufacturers to break into the high-end market, recognizing the issues in the entire loop and co-building the ecosystem with applications is the sole breakthrough.
Looking at iOS’s design, compared to macOS, it restricts application freedom but isn’t a one-size-fits-all solution. It offers various “windows of opportunity” or “unified solutions” to cater to different developers’ needs. The objective is to allow developers to operate within reasonable boundaries, not to drain users’ battery and performance.
Summarizing the principles beyond the technology:
At this point, it seems like a clash between two regimes: one valuing freedom and individual priority, and the other advocating unified arrangement and scheduling. Regardless of the regime type, the ultimate objective must be considered. If the aim is to offer the best device user experience, evidently, the latter regime has been proven right by the market.
Looking back at the history of electronic consumer products, the development has mainly followed two themes: the democratization of professional equipment and the integration of multifunctionality (N in 1 style). The reliance on CPU computation is gradually being replaced by Domain Specific Architecture (DSA). Upon DSA, domain-specific programming languages and compilers are constructed, with GPU and Shader Language in the graphic processing domain serving as prime examples. The era where software reaps the benefits of CPU performance enhancement is drawing to a close, and DSA appears to be the opportunity for the next “great leap” in the coming decade.
M1 epitomizes the dividends brought by regular microarchitecture and manufacturing process, but its impact is magnified due to the subpar performance of competing products. When a product’s core components are supplied by specific manufacturers, its developmental ceiling is essentially predetermined. This underscores the oft-repeated adage that core technologies must be self-controlled. Besides its CPU capabilities, M1 excels in multimedia processing, especially in video stream processing scenarios, outperforming Intel chips substantially. These performance enhancements are attributed to the processor’s performance uplift in specific scenarios.
However, this doesn’t signify the end of the road for performance enhancements based on CPUs. As CPU performance enhancements stagnate, precise understanding of demands and optimizations of matrices and architectural designs to boost performance on existing CPUs become imperative. Profound insights into hardware, compilers, algorithms, and operating systems (both frameworks and kernels) are increasingly crucial. After optimizing business codes to a certain extent, focus inevitably shifts towards the underlying layers.
Accumulated experience from numerous failures is essential to anticipate issues and design optimal architectures and optimization matrices proactively. An optimization matrix refers to the necessity of an ensemble of complementary technologies, not just an OS, to deliver an exceptional experience. This includes IDEs, cloud collaboration, and accurate cognition. Offering a supreme experience is a daunting task, but the more one learns, the more possibilities become apparent. By the same token, maintaining a perpetual “awareness of one’s unawareness” is equally pivotal.
However, all these are contingent upon the designers’ ability to keep pace with their cognition.
Charlie Munger once articulated that investment isn’t merely about scrutinizing financial statements and trend charts. Psychology, sociology, political science, and even biology are intricately linked to it. Only by dismantling the barriers between disciplines and integrating contents from multiple fields without reservations can one perceive a world invisible to others. While I haven’t attained such an enlightenment, Munger’s insights offer invaluable lessons worthy of our learning. Deliberate cross-disciplinary and cross-field practice, coupled with reflective thinking, significantly augments the learning process.
“I leave my sword to those who can wield it.” - Charlie Munger
An individual can move faster, a group can go further.
]]>本文是之前星球里 Yingyun 大佬的文章,由于星球已经关闭,所以把这个关于 OS 性能设计的系列文章发到博客上
Yingyun 是资深性能优化专家,他对于系统优化有非常深入的见解,本身在国内各个手机大厂都呆过,他本人的博客还在休整中,等休整好了我再发出来,目前他在我们的微信群里很活跃,对本文有什么建议或者意见,或者说想咨询问题的可以加我们的微信群(加我微信 553000664,备注博客加群,我会拉你进去)
新开系列文章,OS 架构设计中的各种考量因素。其实不止 OS,在设计任何大型软件都涉及到此类内容。
能力与知识面有限,而且还带了非常主观的看法,肯定有不足之处。希望听到不同的思路与观点,通过观点的碰撞进而达到更进一步的认知。
我认为,Android 与 iOS 在 OS 角度看最核心的差异主要表现为:
他们为什么会有不同的策略决策呢?这跟架构设计时的考量因素有关。一个软件架构设计决策是在当前一系列考虑因素中选择了对当前与可见的未来选择的最合适的决策,是一系列决策的集合。所以,架构的设计上没有绝对的对与错,或者说合理与不合理。因为不同项目、不同决策者所面临的的考虑因素、追求的侧重点是都不尽相同。 如果架构师的水平差不多,把他们互换下情境,那大概率上所做出的决策是差不多的。这说明架构设计是一个工程,是一个技术手艺,它是可以被习得且有规律可循的。
架构设计中的挑战,可能更多的是在于准确理解组织所处的环境、当前与可见未来所要满足的考量因素,并从已有工程实践中找出最合适的方法论或者技术栈。由此可见,考虑因素就起了重要作用,在软件架构设计里要考虑哪些因素呢?包括但不限于,可测试性、组件发布效率、组件开发效率、安全性、可靠性、性能、扩容性等等。有经验的架构师,特别是对类似业务有操盘经验,他更能把握好不同时期应该注重什么,哪些是必须坚持的,哪些是适当放开甚至舍弃的。考虑到边际收益递减,影响因素的考量与决策行为会贯穿整个生命周期,这又是另一个「决策艺术」了。
我们可以总结出:
有意思的是一个考量因素可能会与另外一个有冲突,会造成鱼和熊掌不可兼得局面。比如提高了组件开发效率但有可能会影响到程序性能。那针对一个移动 OS 的设计,当初 Android 与 iOS 的设计者们的考量因素是什么呢?为了回答这个问题,首先要解释 OS 与应用程序以及内核之间的关系。
首先要说明的是 OS 是硬件之上的软件栈的一部分,它与应用程序一道构建了完整的软件程序栈,通过发挥硬件的能力为用户提供服务。从硬件的角度看,甭管 OS 还是应用程序,它们都是软件,只是 OS 的权限比较高,可以在 CPU 的高级别模式下运行,比如用于直接跟硬件打交道、执行中断处理程序等。但是在软件开发者角度来看,OS 与应用程序,那可是完全不一样的。应用程序利用 OS 提供的能力,实现自身的业务需求、或者使用硬件的能力,比如播放音乐、存储数据等。
再拔高一个层次,在用户体验角度来看,应用程序、OS 以及硬件,都是一回事情。 当他们协作出现问题,普通消费者可能就觉得这台设备就不够理想。因此,这三方都有义务互相配合好、互相为彼此提供便利,只有把消费者伺候舒服了,这三家才有可持续的利润可赚。
当我们说到 OS,绝大部分现代 OS 是由内核(Kernel)跟系统服务组成。
现在主流的操作系统有 macOS、Windows 以及基于 Linux 的各种衍生版本。显然,Android 是属于基于 Linux 的衍生版本,当然还有个在开发者圈子中更有名的延伸版本,那就是 Ubuntu。 由于一个完整的 OS 的复杂性与路径依赖特性,往往会将一个延伸版本部署到不同的硬件与不同的应用场景上。有一阵子 Linux 就是以此为标榜,即它辐射到了多少台设备,应用场景有多广之类。能运行一个 OS 跟是否运行得好,即把硬件、应用程序连接起来,提供了最优的用户体验,完全是两码事情。
一个越来越普遍的认知是,不同的应用场景下,所需要的 OS 的机制与策略以及与之紧密配合的应用程序,是完全不一样的。
比如,即使是基于同一个 Linux 内核,用于嵌入式设备、智能手表以及智能手机、大型服务器甚至是智能汽车,它们使用 Linux 的方式与构建在它之上的系统服务均是不一样的,当然应用程序的运行策略也不尽相同。Linux 内核与设备驱动可以理解为硬件与系统服务之间的桥梁,是一个标准的桥梁,但是跑在它上面的车辆与行人,是完全不同的。
这就要引申出另一个非常经典的 OS 设计理念,机制与策略。
UNIX 编程一书中有提到「分离原则:策略同机制分离,接口同引擎分离」,机制提供能力,策略提供能力方法。
其中,Linux 提供的内存管理、进程管理、VFS 层、网络编程接口均是机制。内存分配与释放机制、进程调度器、频率与核分配器以及不同的文件系统,他们均是策略。更进一步,由系统服务识别出哪些进程是跟用户有交互、哪些是后台任务,将此类信息同步到调度器后找出下一个调度周期窗口里的最佳进程,本质上也是基于 Linux 进程管理机制之上的策略。 同样道理,根据未来所需的计算任务需求将其信息通知到 CPU 频率分配器进行动态调整(DVFS),前者是策略后者是机制。
再比如,所有的现代 OS 都支持内存压缩能力,但是不同的 OS 要根据自身的特点来使用此机制,目的是尽可能满足业务特点。iOS 中可以看到针对进程级别的压缩,而 Android 中反倒是依赖 Linux 的 ZRAM。
OS 的机制或许类似,但是策略千差万别,他们之间的差异有多大,取决于 OS 设计者对自身业务的理解。 自身业务,指的是运行在 OS 之上的应用程序,到底要为用户提供什么样的体验与服务。早期的 Android 其实就是桌面机架构,随着国内厂家对它的魔改,反倒是越来越像 iOS 了,越来越符合一个移动设备操作系统所需要的系统能力了。
智能手机、智能汽车的 OS,估计也是同样局面,不可生搬硬套。
策略还有个很有意思的特点,当你实施一个策略的时候会引申出另外一个问题,为此你要引入另一种策略。当一个策略弥补另外一个策略,逐渐会形成一个链条,你会发现你形成了一个闭环的机制。 即,所有的策略同时生效,才能使你的系统发挥出最大的效益。 当我们学习竞品 OS 的策略的时候,一定要记得这一点,否则只学会了皮毛,功能上线后会带来更大的「负优化」。
那具体到 Android 与 iOS,他们的策略设计是从何而来?
苹果推出的操作系统有 macOS,iOS,iPadOS,watchOS 与 tvOS。它们之间并不是单纯的品牌名称差别,而是有具体的策略差异。底层的机制大部分有共享,但是策略却截然不同。iOS 上的后台运行机制与 macOS 就截然不同,iOS 更像是「限制版多任务」,而 macOS 是真正的多任务。所以,iOS 并不是不能实现多任务,而是它的有意为之。
我们所说的安卓,其实是谷歌开源的项目,即 AOSP(Android Open Source Project)。设备厂商拿到 AOSP 与与硬件的相关的代码之后会在此基础上加入自己的各种服务,这是基于它们自身的商业模式、目标用户的理解,所创造出来的。苹果只有一个苹果,但是设备厂家就有很多,它们各自的盈利模式跟对目标用户理解不同,在 AOSP 基础上魔改了一遭,至于哪个好,就让市场先生来做判断吧。
回到技术本身,AOSP 中有大量的机制但是缺乏策略。谷歌把这些策略实现在了 GMS 服务中。 如果你在非大陆地区使用安卓手机,如 Pixel、三星手机,会体验到谷歌的全家桶。大家都会说国内的生态比较烂,垃圾应用比较多,到了海外这可能会缓解。 其实也就那样,谷歌的生态控制跟影响能力跟苹果是没法比的。
连谷歌自身的策略表现不尽人意,那就更为国内厂商发挥拳脚腾出了空间。中国大陆手机出货量累计合是全球最大的,而且竞争尤为激烈。因此它们会非常重视消费者的反馈,各种各样的需求与痛点挖掘,自然不在话下。 简单总结就是,国内厂商更懂消费者的需求,这也是国内厂家对 AOSP 做各种魔改与优化成立的底层逻辑了。
所以当你再次见到有个老板说 “做手机很简单嘛,拿开源安卓跟厂商的方案整合一下就行了”,离他赶紧远一点,跟着他混简直就是枉费青春。
iOS 与安卓之间两者最大的差异来自于策略,他们之间拥有的机制都差不多顶多效率上可能有差异,但更大的差异来自于策略上。
这跟它两刚开始时不同的服务对象与发展目标,导致了技术选型上的巨大差异。
「乔布斯传」与「成为乔布斯」两本书中,关于 iPhone 研发章节中都提到过关于 AppStore 的讨论。起初乔布斯坚持认为移动设备上由于功耗 / 性能与安全的考虑,不允许让三方应用开发程序。由于系统复杂,所以直接采用了 macOS 的内核以此实现多媒体、浏览器,以及播放音频与视频等功能。macOS 内核本身的运行成本较高,在此情况下再让三方应用运行,硬件根本吃不消。他们原本可以基于 iPod 上的系统实现 iPhone,但乔布斯又要实现世人从未见过的智能手机,上马 macOS 也是他不得已的选择。
随着第一代 iPhone 在用户侧的成功(商业上成功还没有开始),大家都在呼吁在 iTunes 上可以下载应用程序。其实第一代黑客们就是这么做的,因为很多 API 是跟 macOS 共享,因此通过一些逆向手段观察到了 iPhone 上编写应用程序的方法。但乔布斯坚决反对,因为还是担心这会破坏安全性跟设备使用体验。从此处就能看出,乔布斯的目的是打造一个拥有完美用户体验的设备,用户的呼吁或者需求,其实是并不是首要的。
但 VP 们不这么想,由于之前的 iTunes + iPod 组合的成功,VP 们私底下开始安排相关的工程研究了。直到后来来乔布斯也没有再坚持反对,只是说自己不再管了。
对 UX 界面的优雅性,特别是图标与界面的一流体验是刻在苹果骨子里的基因。即使是开放出 API,他们也对整个应用运行机制做了修改,从此开始与 macOS 上的程序执行策略有差异了。
由于第一批应用都是苹果自己写的,因此他们积攒了大量在嵌入式设备上良好设计的应用应该是怎样的,也设计出了非常有效的 API 体系。 在这种局面下,苹果有自己的 OS、自己的 IDE、自己的商店系统(当时还是跟 iTunes 共用),自然而然会设计出「最佳应用」应该长什么样。
这里头缺一不可,还记得前面提到过的观点吗? 当你要实施一个策略的时候,可需要另一个策略来解决前一个策略带来的问题,当策略变多的时候就有可能形成了一个环路。
从这儿可看出,为了打造出一个完美的体验,从硬件到软件全部打通,是必然的结果。 这不是为了封闭而封闭,而是为了打造最佳体验而做出的唯一一条路。
反观 07 - 08 年的 Android 阵营,他们还在忙着如何让系统跑起来。
在由 Cheet 著作的 「The Team That Built the Android Operating Systems」书中讲述了安卓从零开始被 Andy Rubin 创建的过程。它是由 Andy Rubin 离职后创业的公司,起初目标是给相机提供 OS。但是随着市场的演变,他们的目标变成了提供给移动设备,特别是手机的移动操作系统。Andy 当初的目标是创建一种可以平衡开发者与设备制造商以及运营商利益的真正意义上的完全开放的操作系统。这就要求它尽可能采用市面上已有的组件,将他们简单改在后适应嵌入式环境后快速部署上线。毕竟是有创业压力嘛,也不可能精雕细琢,只能用快速发展来解决各种体验问题了(主要是这时候 iPhone 还没出来)。再加上从零开始,他们没有配套的 IDE,也只能先提供简单的基于命令行的工具来构建应用程序, 因此从开始他们就缺乏整个应用运行环境的管控能力。不过这也是相对于苹果而言,毕竟两者的目的完全不同。
Android 的目标之一就是有大量应用程序可用,因此照顾到市面上人群最多的开发者是很想当然的思考方式。当时市场上开发者最多的编程语言是 Java,而 Java 在嵌入式领域里也是很受欢迎的。你可能很难相信,07 年的时候嵌入式设备性能很差,但为什么会是受欢迎的编程语言呢? 个人理解原因是,除特殊设备之外大部分设备其实不关心性能,基本维持在能用就行的程度。而且 Java 的可移植性,也大大降低了开发者的负担。为了使 Java 速度更快,Andy 团队还聘请了一位大神重新写一套适合嵌入式设备的虚拟机,这在当时看来都是正确的选择,只是没有 iPhone 出来之前。 不过运气好在,智能手机刚好碰上了黄金的 CPU 单核性能与制程爆发的十年,因此它跟 iOS 相比并没有逊色太多。
在安卓开发的早期,使用的是基于 Eclipse 的构建与开发工具,虽然谈不上非常优秀但是够用。但是痛苦的根源来源于对比,苹果很早之前开始构建自己的 IDE 工具,即 Xcode。这套工具里集成了大量的应用开发与调试以及分发功能,这极大的提高了应用开发工程师的效率。 虽然安卓将开发环境切入到 AndroidStudio 之后相比之前进步了很多,但这主要还是得益于 IntelliJ 本身的优秀,单纯应用开发角度来看跟 Xcode 相比还是弱了一些。Xcode 提供了非常方便的编写与调试程序性能的工具,这会使开发者通过简单的学习就能快速找出程序上低性能的代码段,大大提高了程序的质量而且还使整个过程很愉快,这在安卓上可不是这么一回事了。
由此可见,苹果能够在应用生态管控上的强势,其中一个重要的原因是得益于它的强大的 IDE 工具,提供方便的来帮助开发者解决问题,而不是一味地给他们压力。更进一步,LLVM、Swift 以及 SwiftUI,这些都是苹果了编写出更好地应用而所做的基础工具与语言。 目的当然是为了自身应用生态的发展,为消费者提供最佳的体验。 它的思路是尽可能服务好开发者,让他们编写更能契合系统机制的应用程序,即给你限制又给你解决方案。
设计 OS 的目的是盈利,因此要想办法帮助应用开发者开发好程序。很多 OS 提供了能力之后,应用如何编写就不归他们管了,他们往往会把这个责任放到应用开发者身上,从苹果身上可以看到,这种做法可能不利于整个设备的最优体验,吃亏的还是消费者自己以及厂商,因为把消费者给磨没了。
所以当我看到苹果的强大时候,除了硬件的强大,在软件生态的建设上面的思路是非常值得借鉴的,简单总结就是:
可以看到虽然苹果有绝对的话语权,但同时也提供了远超于行业平局水平的软件服务。 再次强调,提供 OS 只是手段,要认识到与开发者建立怎样的关系、提供怎样的开发者服务(如 IDE),是在认知层面更为有意的事情。
移动设备的最大特性就是可移动,它是由电池供电实现了可移动性。除此之外,由于是手持设备因此散热基本靠被动散热,没有主动散热一说(奇葩的游戏手机与外挂式风扇另说)。现在有两个趋势,其一是晶体管的制作工艺越来越趋近于物理极限,其二是越来越多的功能直接封装在同一颗芯片上。处于活跃状态的晶体管数量变大(或者面积)之后发热量也是蹭蹭往上涨,这在智能手机刚开始普及那一会儿倒不是主要的矛盾。对于智能手机来说,现在更大的矛盾是,功耗与性能的平衡。
活跃的线程多了,就会使 CPU 一直处于工作状态,当然分给用户相关程序的 CPU 时间片也会少一些,性能也就受到影响了。所以移动设备 OS 的设计上,就自然的引申出了要对应用程序的资源使用上的限制,如像服务器、台式机一样完全放开,性能与功耗是无法保证了(当然还有发热)。越是性能跟功耗约束大的设备,对应用程序的管控就越严苛,比如智能手表。
Android 与 iOS 各自均有资源保护机制,Android 中最为常见的当属 OOM 机制了。当 Java 应用的堆内存使用超过一定阈值之后,就会被系统终止。它也有 CPU 使用过度的检测,但从实现上比较简陋,而且只会监听普通应用程序的 CPU 使用量,不监控系统以及由 Native 编写的线程(不用 Java 写)。
iOS 中可谓百花齐放,从 CPU 到内存,甚至 IO 过多写入也有限制,具体为:
显然不止于此(可见未来,iOS 会增加更多限制),但以上是跟普通应用开发者最为密切的。iOS 将这些行为写在了开发者文档,让开发者知道自己的应用被异常退出时的原因。
谷歌对 Android 设计上的” 放松 “ ,可就给国内厂家留出了很多发挥空间。各家都有各自的资源过载保护策略(与 iOS 的类似,仅有或多或少的差异),它们尽可能保护手机不会被垃圾应用给搞坏了。但问题也恰恰出在这部分,由于系统的查杀行为没有明文化,开发者不知道自己的应用为什么会被终止。 即使知道了原因,也无法获取被终止时的调试信息,也就没办法做改进,因为没有哪家会把这类信息开放给应用开发者。
这导致的结果是,应用开发者不得不想出各种各样的所谓黑科技来使自己保活、绕过系统的各种检测机制。本应该由开发者一起共建的生态,现在变成了两家的攻防战了。到最后,是两败俱伤,而其中最受伤的就是消费者。
理想中的世界:
厂商跟开发者应当是合作关系,可能厂商要做更多的事情用于帮助开发者,因为很多能力只有厂家才有。只是一味地责怪质量差是开发者的问题,这种厂商我觉得不太会有竞争力。
在认知维度上,就已经错了。
不同的设备形态所追求的体验要求不同,因此对 OS 的设计要求也不尽相同。在桌面机 OS 中一个应用程序的生命周期完全是由自己掌控的,目的是尽可能发挥程序的能力。能这么设计的原因是桌面机里没有功耗跟散热的限制,很少也会有性能上的瓶颈。对它来说首要考虑的问题是如何把机器的能力榨干。
而在智能手机上却是另一种情况,原因是它有功耗跟发热的限制。与此类似,智能手表上也有功耗与散热的限制,但是它比手机设备更为严苛。谁都不希望手腕里戴着一个会发热的表,而且续航一天都撑不了。除此之外,由于它的性能跟内存受限,不能驻留太多的程序在后台,因此需要有一个集中式管理的模块来统一实现绝大部分常见应用的服务,即所谓的托管式架构。智能汽车虽然没有性能、功耗以及发热的限制,但是它对稳定性的要求非常高。除非是汽车彻底断电,核心系统服务需要保持一直运行,因此对系统防老化的设计尤为重要。
智能手机 OS 设计中一个核心策略是关于生命周期管理的,它决定了应用程序的由生到死的整个过程。Android 的设计上更偏向于桌面机系统,因此策略的设计上比较「宽松」,留给开发者的发挥空间比较多。而 iOS 上限制比较多,一个应用退到后台之后只有 5 秒左右的时间用于执行后台任务,随后便进入到 Suspend 状态。在这种状态下应用程序是得不到 CPU 调度执行,因此在后台的时候应用会比较「安静」。
国内厂家拿到 AOSP 代码之后实现了类似 iOS 的这种 Suspend 机制,不过碍于 AOSP 原生的不支持,因此做了很多让步,这导致了整体效果上不如 iOS 来的彻底。Android 把这种运行策略解释为把应用写好应用开发者的责任,而我觉得这个想法是幼稚且不切实际的。如果这个逻辑成立的话,人类发展历史上都不需要有法律了,这是违背人性的事情。 不过好在谷歌可能意识到了问题,每年的更新中也逐步完善了俗称「冻结」的策略。不过能力奇差,远不及国内厂商所做的改进。AOSP 的进步也是比较缓慢,目测未来两三年内,这部分的进步速度也会一直缓慢,没有实质性的改变。
如果应用在后台被 Suspend 住了,那在 iOS 上如何实现需要后台计算的任务呢? 它引入了 BackgroundTask 的机制,让应用程序申请后台执行任务权限,由系统智能的调度执行应用程序的后台任务。所以,iOS 是给了一套策略让应用程序执行后台任务,但是决定权是交给系统执行的。这有利于系统根据当时手机的不同的状态调度后台任务,比如系统负载比较高的时候就不会执行后台任务了,目的是降低系统整体的负载。系统给每个应用还设置了每日配额的概念,比如一天之内允许你最多执行多少次等等,当然执行时间也是其中一个很重要的考量因素。一般允许执行 30 秒左右,超过之后就会被系统终止了。
但是后台任务不止于计算一种,播放音乐、位置定位,这种需求如何处理? 应用想要使用此类服务,需要在 IDE 中显示声明之后才可以使用,App Store 会检查应用与所申请权限是否匹配,不匹配时不给予通过。App Store 是 iOS 能够实现这套生命周期管理机制的很核心的一环,通过它实现了在上架期与运行期间的质量管控。当发现一个应用的质量较差的时候,会通知开发者让其修复,否则就会下架应用。 继 Suspend 之后系统也会随之终止应用程序,目的是过载保护。最常见的理由是回收内存,毕竟内存芯片非常贵,不上大内存的前提 下只能通过终止应用来腾出更多的内存了。
那我连应用程序都不执行了,又怎么执行后台任务?接收消息呢? 得益于 BackgroundTask 的设计,即使应用被终止了,当条件满足的时候系统还会自动的拉起你的程序执行后台任务。至于消息接收,则是通过消息通知机制来实现。分为两种,一种是在通知栏里能看到具体的内容,只有用户当点击此通知的时候才会唤醒应用程序。另一种则是 VoIP 类应用,可以主动拉起被终止的应用程序。
其实 Android 也有类似的机制,但是需要配合它的 GMS 服务使用,由于不确定的原因这个服务在国内是无法使用。因此国内的 App 不得不又要上各种各样的黑科技、商业合作等手段,使自己的程序在后台保活用于接收消息。这就造成了一个非常畸形的局面,头部应用由于是用户常用的,因此厂商也对它一路开绿灯。而厂家也发现这个现象之后,不断加码各类的服务,把手机当做自己的来用,尽可能榨干系统内存。 那有没有可能厂商自己提供类似的通知服务呢? 有,但是由于建设与运营成本跟自己的销售利润完全不成比例,大家也就只能加大内存容量了,可以把价格压力传递到消费者身上。整机的成本是有上限的,在内存这部分加大那就要在其他地方减弱。国内厂家要突破高端,首先要意识到整个环路的问题所在,与应用共建整个生态才是唯一的破局之道。
纵观 iOS 的设计,相比于 macOS, 限制了应用的自由度,但也不是一刀切方案。而是尽可能的提供了各种各样的「窗口期」或者「统一的解决方案」,以满足开发者的不同需求。目的是尽可能让开发者在合理的范围内做事情,而不是榨干用户的电量与性能。
总结下技术之上的设计原则
写到此处,似乎是两个体制的碰撞,一个崇尚自由,个体优先、一个崇尚统一安排与调度。无论是哪种体制,要看最终目的是什么,如果是要提供最佳的设备用户体验,显然后者体制被市场证明是正确的。
纵观电子消费品的发展史,以专业设备平民化、 N in 1 式的功能整合,两个主旋律方向发展。纯靠 CPU 的计算也会被专用硬件代替,比如 DSA (Domain Specific Architecture)。在 DSA 之上,会构建领域专用的编程语言与编译器,与之最接近的就是图形处理领域,如 GPU 与 Shader Language。 软件吃 CPU 性能提升的红利已经接近了尾声,下一个十年的「大飞跃」,目前看来也就这 DSA 机会点了。
M1 本身代表了正常的微架构与制程工艺带来的红利,只是友商阵营太拉跨,把这种差距拉大了。当一个产品的核心部件由某几个特定供应商提供的时候,基本上也判定了其发展上限。这也就是常说的核心技术要掌握自己手里,是同样的道理。M1 中除 CPU 能力外,它在多媒体处理,特别是视频流的处理场景下相比 Intel 芯片性能指标超出一大截,能实现这些性能提升的得益于处理器的特定场景下的性能提升。
但这不代表基于 CPU 的性能提升已经走到尽头,正是因为 CPU 性能提升的停滞,通过对需求的准确理解,优化矩阵与架构设计来实现在已有 CPU 上的性能提升,变得更为紧迫。对硬件、编译器、算法以及操作系统(框架与内核)的理解,变得越来越重要。因为当你优化到一定层度的业务代码之后,注意力必然会往底层走。
只有相当多的失败的经验,才能未雨绸缪,以高屋建瓴的方式设计出最佳的架构与优化矩阵。 优化矩阵是指为了提供一个极佳的体验,并不是由一个 OS 就能搞定,他要有相配套的其他技术一起支撑才能做好。比如 IDE、比如云端配合、以及正确的认知。 想要提供一个极致的体验,是非常难的事情,但也正因为如此,你会发现了解越多可做的事情就越多。同样道理,也有别样的说法 → 使自己始终处于「知道自己不知道」的状态。
不过以上成立的前提,是设计者的认知要跟得上。
查理芒格说过,投资不是看看财报,看看走势图就能做好的。除经济学与金融学外,心理学、社会学、政治学、甚至生物学都有关系。只有当你踏平学科间的隔阂,不设边界地把多个学科内容融合在一起之后,才能看到别人看不到的世界。 我当然也没达到这种境界,芒格给世人的经验是我们值得学习的宝贵经验。努力跨学科、跨领域的刻意练习,如果能在此基础上做到思考输出,那对学习更有帮助。
我的剑留给能够挥舞它的人 - 查理 芒格
一个人可以走的更快 , 一群人可以走的更远
]]>关于这个 App 是如何获取这个系统权限的,Android 反序列化漏洞攻防史话,这篇文章讲的很清楚,就不再赘述了,我也不是安全方面的专家,但是建议大家多读几遍这篇文章
序列化和反序列化是指将内存数据结构转换为字节流,通过网络传输或者保存到磁盘,然后再将字节流恢复为内存对象的过程。在 Web 安全领域,出现过很多反序列化漏洞,比如 PHP 反序列化、Java 反序列化等。由于在反序列化的过程中触发了非预期的程序逻辑,从而被攻击者用精心构造的字节流触发并利用漏洞从而最终实现任意代码执行等目的。
这篇文章主要来看看 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个库里面提供的 Dex ,看看 App 到底想知道用户的什么信息?总的来说,App 获取系统权限之后,主要做了下面几件事(正常 App 无法或者很难做到的事情),各种不把用户当人了。
另外也可以看到,这个 App 对于各个手机厂商的研究还是比较深入的,针对华为、Oppo、Vivo、Xiaomi 等终端厂商都有专门的处理,这个也是值得手机厂商去反向研究和防御的。
最好我还加上了这篇文章在微信公众号发出去之后的用户评论,以及知乎回答的评论区(问题已经被删了,但是我可以看到:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317,目前为止是 2471 个赞)可以说是脑洞大开(关于 App 如何作恶)。
本文所研究的 dex 文件是从 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个仓库获取的,Dex 文件总共有 37 个,不多,也不大,慢慢看。这些文件会通过后台服务器动态下发,然后在 App 启动的时候进行动态加载,可以说是隐蔽的很,然而 Android 毕竟是开源软件,要抓你个 App 的行为还是很简单的,这些 Dex 就是被抓包抓出来的,可以说是人脏货俱全了。
由于是 dex 文件,所以直接使用 https://github.com/tp7309/TTDeDroid 这个库的反编译工具打开看即可,比如我配置好之后,直接使用 showjar 这个命令就可以
showjar 95cd95ab4d694ad8bdf49f07e3599fb3.dex
默认是用 jadx 打开,就可以看到反编译之后的内容,我们重点看 Executor 里面的代码逻辑即可
打开后可以看到具体的功能逻辑,可以看到一个 dex 一般只干一件事,那我们重点看这件事的核心实现部分即可
一般我们会通过 ServiceManager 的 getService 方法获取系统的 Service,然后进行远程调用
通过 getService 传入 NotificationManagerService 获取 NotificationManager 之后,就可以调用 getActiveNotifications 这个方法了,然后具体拿到 Notification 的下面几个字段
可能有人不知道这玩意是啥,下面这个图里面就是一个典型的通知
其代码如下
可以看到 getActiveNotifications 这个方法,是 System-only 的,普通的 App 是不能随便读取 Notification 的,但是这个 App 由于有权限,就可以获取
当然微信的防撤回插件使用的一般是另外一种方法,比如辅助服务,这玩意是合规的,但是还是推荐大家能不用就不用,它能帮你防撤回,他就能获取通知的内容,包括你知道的和不知道的
这么看来这个应该还是蛮实用的,你个调皮的用户,我发通知都是为了你好,你怎么忍心把我关掉呢?让我帮你偷偷打开吧
App 调用 NotificationManagerService 的 setNotificationsEnabledForPackage 来设置通知,可以强制打开通知
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
然后查看 NotificationManagerService 的 setNotificationsEnabledForPackage 这个方法,就是查看用户是不是打开成功了
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
还有针对 leb 的单独处理~ 细 !
核心和上面那个是一样的,只不过这个是专门针对 vivo 手机的
没有反编译出来,看大概的逻辑应该是打开 App 在 oppo 手机上的通知权限
这个就有点厉害了,在监听 App 的 Notification 的发送,然后进行统计
监听的核心代码
这个咱也不是很懂,是时候跟做了多年 SystemUI 和 Launcher 的老婆求助了....@史工
上面那个是 UdNotificationListenerExecutor , 这个是 NotificationListenerExecutor,UD 是啥?
这个反射调用的 setNotificationListenerAccessGranted 是个 SystemAPI,获得通知的使用权,果然有权限就可以为所欲为
华为也无法幸免,哈哈哈
这个看了半天,应该是专门针对华为手机的,收到 IBackupSessionCallback 回调后,执行 PackageManagerEx.startBackupSession 方法
查了下这个方法的作用,启动备份或恢复会话
拿这个干嘛呢?拿去做数据分析?
获取 SharedPreferences
获取 slog
看核心逻辑是同 usagestates 服务,来获取用户使用手机的数据,难怪我手机安装了什么 App、用了多久这些,其他 App 了如指掌
那么他可以拿到哪些数据呢?应有尽有~,包括但不限于 App 启动、退出、挂起、Service 变化、Configuration 变化、亮灭屏、开关机等,感兴趣的可以看一下:
1 | frameworks/base/core/java/android/app/usage/UsageEvents.java |
上面那个是 UsageEventAllExecutor,这个是 UsageEventExecutor,主要拿用户使用 App 相关的数据,比如什么时候打开某个 App、什么时候关闭某个 App,6 得很,真毒瘤
看样子是注册了 App Usage 的权限,具体 Code 没有出来,不好分析
经吃瓜群众提醒,App 可以通过 Widget 伪造一个 icon,用户在长按图标卸载这个 App 的时候,你以为卸载了,其实是把他伪造的这个 Widget 给删除了,真正的 App 还在 (不过我没有遇到过,这么搞真的是脑洞大开,且不把 Android 用户当人)
这个比较好理解,在 Vivo 手机上加个 Widget
这个好理解,获取 icon 相关的信息,比如在 Launcher 的哪一行,哪一列,是否在文件夹里面。问题是获取这玩意干嘛???迷
小米手机上的桌面 icon 、shorcut 相关的操作,小米的同学来认领
看下面这一堆就知道是和自启动相关的,看来自启动权限是每个 App 都蛋疼的东西啊
看名字就是和关联启动相关的权限,vivo 的同学来领了
直接写了个节点进去
看名字和实现,应该是和华为的耗电精灵有关系,华为的同学可以来看看
猜测和保活相关,Vivo 的同学可以来认领一下
这个看上去像是用户卸载 App 之后,回滚到预置的版本,好吧,这个是常规操作
看名字和实现,也是和卸载回滚相关的
同上
没看懂是干嘛的,核心应该是 Utils.updateSid ,但是没看到实现的地方
看名字应该是解析从远端传来的 Notify Message,具体功能未知
没太看懂这个是干嘛的,像是保活又不像,后面有时间了再慢慢分析
获取 LBS Info
看名字应该是个工具类,写 Settings 字段的,至于些什么应该是动态下发的
Setting 代理??没看懂干嘛的,Oppo 的同学来认领,难道是另外一种形式的保活?
Check aster ?不是很懂
获取 Oppo 用户的 ID?要这玩意干么?
获取 Oppo 手机用户的 username,话说你要这个啥用咧?
配置 Log 的参数
Vivo 浏览器相关的设置,不太懂要干嘛
知乎回答已经被删了,我通过主页可以看到,但是点进去是已经被删了:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317
这里就贴一下安全大佬 sunwear 的评论
]]>一个人可以走的更快 , 一群人可以走的更远
第一次茶话会没有预设主题,预计 1 个小时就可以结束,结果聊了 2 个半小时,光是主理人和嘉宾的个人介绍就花了一个小时。平时大家在群里和星球里交流很多,但是通过语音在线交流还是第一次,再者大家的公司基本上涵盖了 Android 上下游:既有 App 大厂的大佬,也有一线手机厂商的系统大咖,也有芯片公司和造车新势力的资深专家。所以在每个人自我介绍的时候,会聊一些公司相关的东西,再发散一下,其他人再问一些问题,时间就过去了。
后续会不定期举办星球茶话会,主题会更加明确,也会邀请更多嘉宾来一起聊聊,不局限于技术,欢迎大家多多参与和提问。考虑到隐私问题,
本次茶话会没有录屏,下面的内容也不会涉及到各位的隐私,文字稿是根据聊天内容部分还原的。
有时候需要出去跟友商、新赛道的人沟通下,可以看看外部环境都在发生怎样的变化。首要目的不是为了跳槽(如果有好机会,跳也无妨),而是为了让自己在人力资源市场上有比较高的竞争力。
这里首先要区分职业与工作的区别。我是软件工程师,这是我的职业。我在 A 工作任职负责某个模块,这是我的工作。这两个是不一样的,如果你能把两者区分开(前提是能区分得开),那在择业、面对公司变化的时候都能从容应对。
最理想情况下,应该追求自己喜欢的职业,因为它伴随你绝大部分时间,如果这个职业本身不让你感到兴奋,你不太可能做出比较好的成绩,进而无法获得比较高的回报。
遇到好的工作,那就看天意了。这很复杂,与很多很多因素有关系。当你遇到面试不顺利,只能说这个工作不适合你,没有缘分,无法进一步说明更多的事情。
不同的公司在不同的行业赛道、竞争格局上所采用的策略是不一样的。微观上,你的部门领导的风格以及同事们的结构,都影响到了你具体做事的环境。不同环境所追求的「回报最优值」是不一样的。
有的是需要你做到行业 5%,得到组织认可。有的是能把问题搞定就行而不关心如何搞定的,得到组织认可。
要么直接硬刚这种环境,按照你认为对的方式行事,要么就适应它。无论选择哪种,要知道局面是什么局面,而不是遇到与自己想象不一样的时候要么怀疑自己,要么以为这个世界就是这样。
对于优化业务来说,它是有底层逻辑可寻的。而这些底层逻辑遇到不同的技术栈、不同的局面的时候会藏的比较深。平时学习中,要努力找出这种底层不变的东西,将可变的东西套进去,一是增强对新技术的判断力,二是可以举一反三提出更进一步的优化方案。
不要在新技术的表面上游荡,一是自己觉得累,二是这没啥意义。
优化做到一定深度,肯定是往底层走。比如编译器、硬件特性,甚至硬件整合。因为这些东西直接决定了程序的速度,所以是绕不开的一道坎。
最后就是:学习图形开发的人一定要沉得住气耐得住寂寞,多看书多练习,不然很难学得精通,这是一个长期的过程,不是看几本书就能精通的。学习 GPU 架构的人,国内基本没有一家像样的 GPU 供应商,GPU 方面资料会很少,所以一定要多到外网去收集学习资料,可以多用 GPU 调试工具调试去分析应用程序,去了解 GPU 内部的 Counters,如 DS5 Streamline、Snapdragon Profiler 等,另外在没有很全面的学习资料的前提下,去学习一门更加底层的图形开发技术对学习和了解 GPU 架构也很重要,如 Vulkan,这会有助于自顶而下的去了解 GPU 内部的工作原理。
文章地址:https://presence.feishu.cn/docs/doccn71hTTKbaRGF8RvD2XzLJEK,里面列举了作者总结的 S、A、B、C 类人才,大家可以对照一下自己做事的方式,看看更高级别的人才是怎么做事的
豆瓣地址:https://book.douban.com/subject/3652388/
这本书主要介绍系统软件的运行机制和原理,涉及在 Windows 和 Linux 两个系统平台上,一个应用程序在编译、链接和运行时刻所发生的各种事项,包括:代码指令是如何保存的,库文件如何与应用程序代码静态链接,应用程序如何被装载到内存中并开始运行,动态链接如何实现,C/C++运行库的工作原理,以及操作系统提供的系统服务是如何被调用的。每个技术专题都配备了大量图、表和代码实例,力求将复杂的机制以简洁的形式表达出来。本书最后还提供了一个小巧且跨平台的 C/C++运行库 MiniCRT,综合展示了与运行库相关的各种技术。
对装载、链接和库进行了深入浅出的剖析,并且辅以大量的例子和图表,可以作为计算机软件专业和其他相关专业大学本科高年级学生深入学习系统软件的参考书。同时,还可作为各行业从事软件开发的工程师、研究人员以及其他对系统软件实现机制和技术感兴趣者的自学教材。
本书堪称是软件调试的“百科全书”。作者围绕软件调试的“生态”系统(ecosystem)、异常(exception)和调试器 3 条主线,介绍软件调试的相关原理和机制,探讨可调试性(debuggability)的内涵、意义以及实现软件可调试性的原则和方法,总结软件调试的方法和技巧。
豆瓣地址:https://book.douban.com/subject/30379453/
第 1 卷主要围绕硬件技术展开介绍。全书分为 4 篇,共 16 章。第一篇“绪论”(第 1 章),介绍了软件调试的概念、基本过程、分类和简要历史,并综述了本书后面将详细介绍的主要调试技术。第二篇“CPU 及其调试设施”(第 2 ~ 7 章),以英特尔和 ARM 架构的 CPU 为例系统描述了 CPU 的调试支持。第三篇“GPU 及其调试设施”(第 8 ~ 14 章),深入探讨了 Nvidia、AMD、英特尔、ARM 和 Imagination 这五大厂商的 GPU。第四篇“可调试性”(第 15 ~ 16 章),介绍了提高软件可调试性的意义、基本原则、实例和需要注意的问题,并讨论了如何在软件开发实践中实现可调试性。
本书理论与实践紧密结合,既涵盖了相关的技术背景知识,又针对大量具有代表性和普遍意义的技术细节进行了讨论,是学习软件调试技术的宝贵资料。本书适合所有从事软件开发工作的读者阅读,特别适合从事软件开发、测试、支持的技术人员,从事反病毒、网络安全、版权保护等工作的技术人员,以及高等院校相关专业的教师和学生学习参考。
书籍信息:https://book.douban.com/subject/35233332/
第 2 卷分为 5 篇,共 30 章,主要围绕 Windows 系统展开介绍。第一篇(第 1- 4 章)介绍 Windows 系统简史、进程和线程、架构和系统部件,以及 Windows 系统的启动过程,既从空间角度讲述 Windows 的软件世界,也从时间角度描述 Windows 世界的搭建过程。第二篇(第 5-8 章)描述特殊的过程调用、垫片、托管世界和 Linux 子系统。第三篇(第 9-19 章)深入探讨用户态调试模型、用户态调试过程、中断和异常管理、未处理异常和 JIT 调试、硬错误和蓝屏、错误报告、日志、事件追踪、WHEA、内核调试引擎和验证机制。第四篇(第 20-25 章)从编译和编译期检查、运行时库和运行期检查、栈和函数调用、堆和堆检查、异常处理代码的编译、调试符号等方面概括编译器的调试支持。第五篇(第 26-30 章)首先纵览调试器的发展历史、工作模型和经典架构,然后分别讨论集成在 Visual Studio 和 Visual Studio(VS)Code 中的调试器,最后深度解析 WinDBG 调试器的历史、结构和用法。
本书理论与实践结合,不仅涵盖了相关的技术背景知识,还深入研讨了大量具有代表性的技术细节,是学习软件调试技术的珍贵资料。
这本书应当还有卷 3,此卷里会讲基于 Linux 平台的调试方法。
The Performance 是一个分享 Android 开发领域性能优化相关的圈子,主理人是三个国内一线手机厂商性能优化方面的一线开发者,有多年性能相关领域的知识积累和案例分析经验,可以提供性能、功耗分析知识的一站式服务,涵盖了基础、方法论、工具使用和最宝贵的案例分析
星球免费版,定位是知识分享和交流,也可以微信扫码加入
]]>本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录
一个线程的状态不属于 Running 或者 Runnable 的时候,那就是 Sleep 状态了(严谨来说,还有其他状态,不过对性能分析来说不常见,比如 STOP、Trace 等)。
在 Linux 中的Sleep 状态可以细分为 3 个状态:
在 Systrace/Perfetto 中,Sleep 状态指的是 Linux 中的TASK_INTERUPTIBLE,trace 中的颜色为白色。Uninterruptible Sleep 指的是 Linux 中的 TASK_UNINTERRUPTIBLE,trace 中的颜色为橙色。
本质上他们都是处于睡眠状态,拿不到 CPU的时间片,只有满足某些条件时才会拿到时间片,即变为 Runnable,随后是 Running。
TASK_INTERRUPTIBLE 与 TASK_UNINTERRUPTIBLE 本质上都是 Sleep,区别在于前者是可以处理 Signal 而后者不能,即使是 Kill 类型的Signal。因此,除非是拿到自己等待的资源之外,没有其他方法可以唤醒它们。 TASK_WAKEKILL 是指可以接受 Kill 类型的Signal 的TASK_UNINTERRUPTIBLE。
Android 中的 Looper、Java/Native 锁等待都属于 TAKS_INTERRUPTIBLE,因为他们可以被其他进程唤醒,应该说绝大部分的程序都处于 TAKS_INTERRUPTIBLE 状态,即 Sleep 状态。 看看 Systrace 中的一大片进程的白色状态就知道了(trace 中表现为白色块),它们绝大部分时间都是在 Runnning 跟 Sleep 状态之间转换,零星会看到几个 Runnable 或者 UninterruptibleSleep,即蓝色跟橙色。
似乎看来 TASK_INTERUPTIBLE 就可以了,那为什么还要有 TASK_UNINTERRUPTIBLE 状态呢?
中断来源有两个,一个是硬件,另一个就是软件。硬件中断是外围控制芯片直接向 CPU 发送了中断信号,被 CPU 捕获并调用了对应的硬件处理函数。软件中断,前面说的 Signal、驱动程序里的 softirq 机制,主要用来在软件层面触发执行中断处理程序,也可以用作进程间通讯机制。
一个进程可以随时处理软中断或者硬件中断,他们的执行是在当前进程的上下文上,意味着共享进程的堆栈。但是在某种情况下,程序不希望有任何打扰,它就想等待自己所等待的事情执行完成。比如与硬件驱动打交道的流程,如 IO 等待、网络操作。 这是为了保护这段逻辑不会被其他事情所干扰,避免它进入不可控的状态
。
Linux 处理硬件调度的时候也会临时关闭中断控制器、调度的时候会临时关闭抢占功能,本质上为了 防止程序流程进入不可控的状态
。这类状态本身执行时间非常短,但系统出异常、运行压力较大的时候可能会影响到性能。
https://elixir.bootlin.com/linux/latest/ident/TASK_UNINTERRUPTIBLE
可以看到内核中使用此状态的情况,典型的有 Swap 读数据、信号量机制、mutex 锁、内存慢路径回收等场景。
首先要认识到 TASK_INTERUPTIBLE、TASK_UNINTERRUPTIBLE 状态的出现是正常的,但是如果这些这些状态的累计占比达到了一定程度,就要引起注意了。特别是在关键操作路径上这类状态的占比较多的时候,需要排查原因之后做相应的优化。 分析问题以及做优化的时候需要牢牢把握两个关键点,它类似于内功心法一样:
你需要知道是什么原因导致了这次睡眠,是主动的还是被动的?如果是主动的,通过走读代码调查是否是正常的逻辑。如果是被动的,故事的源头是什么? 这需要你对系统有足够多的认识,以及分析问题的经验,你需要经常看案例以增强自己的知识。
以下把 TASK_INTERUPTIBLE 称之为 Sleep,TASK_UNINTERRUPTIBLE称之为 UninterruptibleSleep,目的是与 Systrac 中的用词保持一致。
初期分析 Sleep 与 UninterruptibleSleep 状态的经验不足时你会感到困惑,这种困惑主要是来自于对系统的不了解。你需要读大量的框架层、内核层的代码才能从 Trace 中找出蛛丝马迹。目前并没有一种 Trace 工具能把整个逻辑链路描述的很清楚,而且他们有时候还有不准的时候,比如 Systrace 中的 wakeup_from 信息。只有广泛的系统运行原理做为支持储备,再结合 Trace 工具分析问题,才能做到准确定位问题根因。否则就是我经常说的「性能优化流氓」,你说什么是什么,别人也没法证伪。反复折磨测试同学复测,没测出来之后,这个问题也就不了了之了。
本文没办法列举完所有状态的原因,因此只能列举最为常见的类型,以及典型的实际案例。更重要的是,你需要掌握诊断方法,并结合源代码来定位问题。
wakeup from tid: ***
查看唤醒线程Sleep 最常见的有图 1(UIThread 与 RenderThread 同步)的情况与图 2(Binder 调用)的情况。 Sleep 状态一般是由程序主动等待某个事件的发生而造成的,比如锁等待,因此它有个比较明确的唤醒源。比如图 1,UIThread 等待的是 RenderThread,你可以通过阅读代码来了解这种多线程之间的交互关系。虽然最直接,但是对开发者的要求非常高,因为这需要你熟读图形栈的代码。这可不是一般的难度,是追求的目标,但不具备普适性。
更简单的方法是通过所谓的 wakeup from tid: ***
来调查线程之间的交互关系。从前面的 Runnable 文章 中讲过,任何线程进入 Running 之前会先进入到 Runnable 状态,由此再转换成 Running。从 Sleep 状态切换到 Running,必然也要经过 Runnable。
进入到 Runnable 有两种方式,一种是 Running 中的程序被抢占了,暂时进入到 Runnable。还有一种是由另外一个线程将此线程(处于 Sleep 的线程)变成了 Runnable。
我们在调查Sleep 唤醒线程关系的时候,应用到的原理是第二种情况。在 Systrace 中这种是被 wakeup from tid: ***
信息所呈现。线程被抢占之后变成 Runnable,在 Systrace 中是被 Running Instead
呈现。
需要特别注意的是 wakeupfrom
这个有时候不准,原因是跟具体的 tracepoint 类型有关。分析的时候要注意甄别,不要一味地相信这个数据是对的。
方法 1 适合用来看程序到底在执行什么操作进入到这种状态,是 IO 还是锁等待?球里连载 Simpleperf 工具的使用方法,其中「Simpleperf 分析篇 (1): 使用 Firefox Profiler 可视分析 Simpleperf 数据」介绍了可以按时间顺序看函数调用的可视化方法。其他使用也会陆续更新,直接搜关键字即可。
方法 2 是个比较笨的方法,但有时候也可以通过它找到蛛丝马迹,不过缺点是错误率比较高。
本质上UninterruptibleSleep 也是一种 Sleep,因此分析 Sleep 状态时用到的方法也是通用的。不过此状态有两个特殊点与 Sleep 不同,因此在此特别说明。
IO 等待好理解,就是程序执行了 IO 操作。最简单的,程序如果没法从 PageCache 缓存里快速拿到数据,那就要与设备进行 IO 操作。CPU 内部缓存的访问速度是最快的,其次是内存,最后是磁盘。它们之间的延迟差异是数量级差异,因此系统越是从磁盘中读取数据,对整体性能的影响就越大。
非 IO 等待主要是指内核级别的锁等待,或者驱动程序中人为设置的等待。Linux 内核中某些路径是热点区域,因此不得不拿锁来进行保护。比如Binder 驱动,当负载大到一定程度,Binder 的内部的锁竞争导致的性能瓶颈就会呈现出来。
谷歌的 Riley Andrews(riandrews@google.com) 15年左右往内核里提交了一个 tracepoint 补丁,用于记录当发生 UninterruptibleSleep 的时候是否是 IO 等待、调用函数等信息。Systrace 中的展示的 IOWait 与 BlockReason,就是通过解析这条 tracepoint 而来的。这条代码提交的介绍如下(由于这笔提交未合入到 Linux 上游主线,因此要注意你用的内核是否单独带了此补丁):
1 | sched: add sched blocked tracepoint which dumps out context of sleep. |
在 ftrace(Systrace 使用的数据抓取机制) 中的被记录为
1 | sched_blocked_reason: pid=30235 iowait=0 caller=get_user_pages_fast+0x34/0x70 |
这句话被 Systrace 可视化的效果为:
主线程中有一段 Uninterruptible Sleep 状态,它的 BlockReason 是 get_user_pages_fast
。它是一个 Linux 内核中函数的名字,代表着是线程是被它切换到了 UninterruptibleSleep 状态。为了查看具体的原因,需要查看这个函数的具体实现。
1 | /** |
从函数解释上可以看到,函数首先是通过无锁的方式pin 应用侧的 pages,如果失败的时候不得不尝试持锁后走慢速执行路径。此时,无法持锁的时候那就要等待了,直到先前持锁的人释放锁。那之前被谁持有了呢?这时候可以利用之前介绍的Sleep 诊断方法,如下图。
UninterruptibleSleep 状态相比 Sleep 有点复杂,因为它涉及到 Linux 内部的实现。可能是内核本身的机制有问题,也有可能是应用层使用不对,因此要联合上层的行为综合诊断才行。毕竟内核也不是万能的,它也有自己的能力边界,当应用层的使用超过其边界的时候,就会出现影响性能的现象。
优化方法
内存是个非常有意思的东西,由于它的速度比磁盘快,因此 OS 设计者们把内存当做磁盘的缓存,通过它来避免了部分IO操作的请求,非常有效的提升了整体 IO 性能。有两个极端情况,当系统内存特别大的时候,绝大部分操作都可以在内存中执行,此时整体 IO 性能会非常好。当系统内存特别低,以至于没办法缓存 IO 数据的时候,几乎所有的 IO 操作都直接与器件打交道,这时候整体性能相比内存多的时候而言是非常差的。
所以系统中的内存较少的时候 IO 等待的概率也会变高。所以,这个问题就变成了如何让系统中有足够多的内存?如何调节磁盘缓存的淘汰算法?
优化方法
系统中的内存是被各个进程所共用。当app 只考虑自己,肆无忌惮的使用计算资源,必然会影响到其他程序。这时候系统还是会回来压制你,到头来亏损的还是自己。 不过能想到这一步的开发者比较少,也不现实。明文化的执行系统约定,可能是个终极解决方案。
当线程处于 UninterruptibleSleep 非 IO等待状态(即内核锁),而持有该锁的其他线程因 CPU 调度原因,较长时间处于 Runnable 状态。这时候就出现了有意思的现象,即使被等待的线程处于高优先级,它的依赖方没有被调度器及时的识别到,即使是非常短的锁持有,也会出现较长时间的等待。
规避或者彻底解决这类问题都是件比较难的事情,不同厂家实现了不同的解决方案,也是比较考虑厂家技术能力的一个问题。
线程状态 | 描述 |
---|---|
S | SLEEPING |
R、R+ | RUNNABLE |
D | UNINTR_SLEEP |
T | STOPPED |
t | DEBUG |
Z | ZOMBIE |
X | EXIT_DEAD |
x | TASK_DEAD |
K | WAKE_KILL |
W | WAKING |
D | K |
D | W |
共享同一个 mm_struct 的线程同时执行 mmap() 系统调用进行 vma 分配时发生锁竞争。
mmap_write_lock_killable() 与 mmap_write_unlock() 包起来的区域就是由锁受保护的区域。
1 | unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr, |
]]>一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录
Trace 中显示绿色,表示线程处于运行态
这是最常见的一种方式,当然不排除平台有bug,有时候厂商在libc、syscal等高频核心函数,加了一些逻辑导致了代码运行时间长。
优化建议: 优化逻辑、算法,降低复杂度。为了进一步判断具体是哪个函数耗时,可使用 AS CPU Profiler、simpleperf,或者自己通过 Trace.begin/end() API 添加更多 tracepoint 点
当然不排除有的时候平台有bug,在关键的libc或内核函数加了一些逻辑
Trace 中看到 「Compiling」字眼时可能意味着它是解释执行方式进行。刚安装的应用(未做 odex)的程序经常会出现这种情况
优化建议: 使用 dex2oat 之后的版本试试,解释执行方式下的低性能暂无改善方法,除非执行 dex2oat 或者提高代码效率本身
除此之外,使用了编程语言的某种特性,如频繁的调用 JNI,反复性反射调用。除了通过积攒经验方式之外,通过工具解决的方法就是通过 CPU Profiler、simpleperf 等工具进行诊断
对 CPU Bound 的操作来说跑在小核可能没法满足性能需求,因为小核的定位是处理非UX 强相关的线程。不过 Android 没办法保证这一点,有时候任务就是会安排在小核上执行。
优化建议:线程绑核、SchedBoost 等操作,让线程跑尽量跑更高算力的核上,比如大核。有时候即使迁核了也不见效,这时候要看看频率是否拉得足够高,见“原因 4”
优化建议:
优化建议:
手机因结构原因导致散热能力差或温升参数过于激进时,为了保护体验跟不烫伤人,几乎所有手机厂家的系统会限制 CPU 频率或者直接关核。排查思路是首先需要找到触发温升的原因。
温升的排查的第一步,首先要看是外因导致还是内因导致。外因是指是否由外部高温导致,如太阳底下,火炉边;往往夏天的时候导致手机发热的情况越严重
内因主要由 CPU、Modem、相机模组或者其他发热比较厉害的器件导致的。以 CPU 为例,如果后台某个线程吃满 CPU,那就首先要解决它。如果是前台应用负载高导致大电流消耗,同样道理,那就降低前台本身的负载。其他器件也是同样道理,首先要看是否是无意义的运行,其次是优化业务逻辑本身
除此之外,温升参数过于激进的话导致触发限频关核的概率也会提高,因此通过与竞品对比等方式调优温升参数本身来达到优化目的
优化建议:
ARM 处理器在相同频率下不同微架构的配置导致的性能差异是非常明显的,不同运行频率、L1/L2 Cache 的容量均能影响 CPU 的 MIPS(Million Instructions Per Second) 执行结果。
优化思路有两条:
第一条比较难,大部分应用开发者来说也不太现实,系统厂商如华为,方舟编译器优化 JNI 的思路本质是不改应用代码情况下提高代码执行效率来达到性能上的提升
第二条可以通过 simpleperf 等工具,找到热点代码或者观察 CPU 行为后做进一步的改善,如:
]]>一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录
Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长 中讲解了导致 CPU 的 Running 状态耗时久的原因与优化方法,这一节介绍 Runnable 状态切换原理与对应的排查与优化思路。在 Systrace 中显示为蓝色,表示线程处于 Runnable,等待被 CPU 调度执行。
从图 2 可知,一个 CPU 核在某个时刻只能执行一个线程,因此所有待执行的任务都在一个「可执行队列」里排队,一个 CPU 核就有一个队列。能插入到这个队列里排队的,代表着这个线程除了 CPU 资源,其他资源均已获取,如 IO、锁、信号量等。处于「可执行队列」的时候,线程的状态就会被置为 RUNNABLE,也就是 Systrace 里看到的 Runnable 状态。
Linux 内核是通过赋予不同线程执行时间片并通过轮转的方式来达到同时执行多个线程的效果,因此当一个 Running 中的线程的时间片用完时(通常是 ms 级别)将此线程置为 Runnable,等待下一次被调度。也有比较特殊的情况,那就是抢占。有些高优先级的线程可以抢占当前执行的线程,而不必等到此线程的时间片到期。
当一个 CPU 有多个核的时候显然可以多个核同时工作,这时候不必都在一个 CPU 核上排队,根据负载情况(也就是排队情况),将线程迁移到其他核执行是必要的操作。掌管这些调度策略的,是通过 Linux 的调度器来实现的,它具体通过多个调度类(Schedule Class)来管理不同线程的优先级,常见的有:
这个可能不止是一个线程,甚至是多个。特别是涉及到 UI 相关的任务,这种情况就更为复杂了。AOSP 体系下典型的一帧绘制是经过 UI Thread → Render Thread → SurfaceFlinger → HWC(参考 图 3),其中任何一个线程被 Runnable 阻塞导致没有在规定时间内完成渲染任务,都将会导致界面的卡顿(也就是掉帧)。
我们从实践中总结出以下 5 大门类,系统层面出异常的原因较多,但也见过应用自身逻辑导致 Runnable 过长情况。
优化思路:
我们在实践中见到过不少应用因为设置错了优先级反而导致更卡。原因比较复杂,可能开发者所使用的机器用当时的优先级策略没问题,但是在别的厂商的调度器(头部大厂基本都有自己改动调度器)下就会出现水土不兼容的情况。一般情况下,三方应用开发者不建议直接调用这类 API,弄巧成拙,屡见不鲜。
长远看来更靠谱的方式是合理安排自己的任务模型,不要把对实时性要求很高的任务放到 worker 线程上。
有时候为了让线程运行得更快,会把线程绑定到大核,在前面解决 Running 时间长时也有建议绑大核,但是绑核一定要谨慎,因为一旦把线程绑定在某个核,表示线程只能运行在这个核上即使其它核很空闲。如果多个线程都绑定在某个核,当这个核很繁忙调度不过来时,这些线程就会出现 Runnable 时间很长的情况。所以绑核一定要谨慎!下面是绑核需要注意的一些事项:
重申下,Runnable 是指在 CPU 核上的排队耗时,按常识可可知道排队长、频繁排队时出问题概率也就越高。一个绘制任务所依赖的线程数量越多,出问题的概率也越高,因为排队次数变多了嘛。
软件架构不止要满足业务需求,也要在性能、扩展性方面上做思考,从上面推导可知,如果你程序编程模型需要大量线程协同运行来完成关键操作,如绘制,那出问题的概率就越高。
最常见的有,两个线程之间有频繁的有通讯与等待(线程 A 把任务转移到线程 B 执行,A 等待 B 任务执行完后被唤醒), CPU 繁忙时很容易打出 Runnable 等待状态,CPU 越忙概率越高。
优化思路:
从上述的调度原理可知,如果大量任务挤在一个核的「可执行队列」上,显然越是后面,优先级越低的任务排队时间就越长。
排查的时候你可以在 Perfetto/Systrace 的 CPU 核维度任务上,即使在放大后的界面看到排满了密密麻麻的任务,这基本上就意味着系统整体负载较高了。通过计算,可算出 CPU 每时刻的使用量,基本上都会在 90%以上。 你可以通过选择一个区间,以时间来排序,看看都在执行什么任务,以此来逐个排查同时执行大量程序的原因是什么。
简单总结就是,同时执行的任务太多了,主要原因来自两方面:
应用自身就把 CPU 资源都给占满了,狂开十来个线程来做事情,即使是头部大厂也会做这种事。
优化建议:
有的厂商 ROM 自己本身就有很多任务,设计不合理的话自己家程序就吃满了大量资源,导致留给应用运行的资源较少。还有些是管控措施设计的一般,以至于留给了大量流氓应用可乘之机,各路神仙利用自己的「黑科技」在后台保活后进行各种拉活同步操作。
厂家除了要优化自身服务,以做到「点到为止」外,可以实现如下功能来尽可能把资源分配合理化,让出更多资源给前台应用。
每一个优化说简单也简单,说难也难,依赖厂家的技术积累。
排队做核酸检测一样,检测窗口多的队列排队时间少。CPU 算力差、关核、限频,导致 Runnable 的概率也更高。通常的原因有:
其中:
几乎所有的厂家都做了调度器优化方面的工作,虽然概率小,但也有可能会出异常。场景锁频锁核机制有问题、内核各种 governor 的出问题的时候,会出现明明 CPU 的其他核都很闲,但任务都挤在某几个核上。
系统开发者能做的就是把基础「可观测性技术」建好,出问题时可以快速诊断,因为这类问题一是不好复现,二是现象出现时机较短,可能立马就恢复了。
有些过渡期的芯片,如最近推出的骁龙 8Gen1 与 天玑 9000,会有非常奇葩的运行限制。32 位的程序只能运行某个特定微架构上,64 位的则畅通无阻。且先不说这种「脑残设计」是处于什么所谓「平衡」,他带来的问题是,当你用的应用大量还是 32 位的时候,很多任务(以进程为单位)都挤在某个核心上运行,结合前面的理论,都挤在一起,出现 Runnable 的概率就更高。
]]>一个人可以走的更快 , 一群人可以走的更远
In his book Hackers & Painters, Paul Graham asserted, “The disparity in the efficiency of languages is becoming more pronounced, hence the rising importance of profilers. Currently, performance analysis isn’t given the attention it deserves. Many still seem to hold onto the belief that the key to accelerating program execution lies in developing compilers that generate faster code. As the gap between code efficiency and machine performance widens, it will become increasingly apparent that enhancing the execution speed of application software hinges on having a good profiler to guide program development.” by Paul Graham, Hackers & Painters
A Google search for “Android optimization tools” yields an abundance of related content. The issue with these results is that they either contain highly repetitive content or directly explain usage methods. Rarely do they introduce a holistic architecture, inadvertently instilling a misguided belief of “one tool fixes all”. Drawing from the extensive experience of my team, I can assert that in the realm of performance analysis, no such magic bullet tool exists. Tools evolve, old problems re-emerge in new forms, and without mastering core logic, one remains on the technological surface.
This article first systematically untangles the observability technology in performance analysis, encompassing data types, capture methods, and analysis techniques. Subsequently, we introduce the “big three” analysis tools provided by Google. The aim is to impart immutable theoretical knowledge and corresponding tools available in the Android environment to the reader. This wealth of information can facilitate a more direct application of predecessors’ experiences, circumventing unnecessary detours.
It’s crucial to note that there are certainly more than these three tools available for performance optimization. However, these three are our “go-to first-hand tools”. Prior to delving into further analysis, you’ll find yourself dependent on these three tools for bottleneck identification. Subsequent analyses, tailored to distinct domain characteristics, should then leverage corresponding tools.
You’ve likely been asked similar questions by colleagues or bosses more than once. The most primitive idea might be to obtain the relevant logs and analyze them one by one. Based on past experience, one would search for clues by looking for keywords. If the desired information is not available, the next step is to add logs and attempt to reproduce the issue locally. This approach is not only time-consuming and laborious, but also wastes developmental resources. Have you ever wondered if there is a more efficient method in the industry? A method that can improve efficiency by an order of magnitude, allowing us to spend our time solving problems instead of on mundane, repetitive physical tasks?
Of course, there is (otherwise this article wouldn’t exist)—we refer to it as observability techniques.
As the computer industry has evolved, pioneers in computing have devised a category known as “observability techniques.” It involves utilizing tools to observe the intricate details of complex systems’ operations—the more detailed, the better. Mobile operating systems evolved from embedded systems. Nowadays, the computing power of mid-to-high-end Android phones can catch up with a mainframe from two decades ago, and the resulting software complexity is also immense.
Employing a well-designed and smoothly operating observability technique can significantly accelerate software development efficiency. This is because, despite using a variety of preemptive static code detection and manual code reviews, it is impossible to block software issues 100%. Problems only become apparent after the software is run in a real environment, which might be an automated test case of yours. Even then, you still need to sift through your logs and re-read code to identify the problem. For these reasons, every engineering team needs a fully functional observability tool as one of their fundamental infrastructures.
Observability is a systematic engineering effort that allows you to delve deeper into occurrences within the software. It can be used to understand the internal operational processes of software systems (especially complex business logic or interactive systems), troubleshoot, and even optimize the program by identifying bottlenecks. For complex systems, understanding the entire operational process through code reading can be challenging. A more efficient approach is to utilize observability tools to obtain the software’s operational status most intuitively.
We will explore data types, data acquisition methods, and analysis methods to help you understand observability techniques in the sections below.
Logs can be in the form of key-value pairs, JSON, CSV, relational databases, or any other formats. We recreate the entire state of the system at the time it was running through logs to solve a specific issue, observe the operation of a module, or even depict the behavioral patterns of system users. In observability technology, log types are classified into Log, Metric, and Trace types.
Logs are the most rudimentary form of data recording, typically noting what happened at what time in which module, whether the log level is a warning or an error. Nearly all systems, whether embedded devices or computers in cars, utilize this form of log. It is the simplest, most direct, and easiest to implement. Almost all Log types are stored as strings, presenting data in lines of text. Logs are the most basic type, and through conversion, can be turned into Metric or Trace types, though the conversion process can become a bottleneck when dealing with massive amounts of data.
Different log types are usually distinguished by error, warning, and debug levels. Naturally, error logs are your primary concern. However, in practice, this classification is not always strict, as many engineers do not differentiate between them, possibly due to a lack of classification analysis for different log levels in their engineering development environment. In summary, you can grade Log types according to your objectives. It acts like an index, enhancing the efficiency of problem analysis and target information location.
Metric types are more focused compared to Log types, recording numerical changes in a particular dimension. Key points are the “dimension” and “numerical change.” Dimensions could be CPU usage, CPU Cluster operation frequency, or context switch counts. Numerical changes can be instant values at the time of sampling (snapshot type), the difference from the previous sampling, or aggregated statistical values over a period. Statistics are often used in practice, such as when wanting to observe the average CPU usage five minutes before an issue occurred. In this case, an arithmetic mean or weighted average calculation of all values within these five minutes is required.
Aggregation is a useful tool because it’s not possible for a person to analyze all Metric values individually. Determining the existence of a problem through aggregation before conducting detailed analysis is a more economical and efficient method.
Another benefit of the Metric type is its fixed content format, allowing data storage through pre-encoding, utilizing space more compactly and occupying less disk space. The most straightforward application is data format storage; Metric types, using integers or floating numbers of fixed byte data, are more space-efficient than Log types, which generally use ASCII encoding.
In addition to specific values, enumeration values can also be stored (to some extent, their essence is numerical). Different enumeration values represent different meanings, possibly indicating on and off statuses, or different event types.
Trace types indicate the time, name, and duration of an event. Relationships among multiple events identify parent-child or sibling connections. Trace types are the most convenient data analysis method when dissecting complex call relationships across multiple threads.
Trace types are particularly suitable for Android application and system-level analysis scenarios because they can diagnose:
In the design of Android’s application running environment, an application can’t perform all functionalities independently; it requires extensive interaction with the SystemServer. Communication with the SystemServer is facilitated through Binder, a communication method detailed later in this article. For now, understand that it involves cross-process calling. Accurate restoration of call relationships requires data from both ends, making Trace the optimal information recording method.
You can manually add starting and ending points for Trace types and insert multiple intervals within a function. With pre-compilation technology or language features, Trace intervals can automatically be instrumented at the beginning and end of functions. In an ideal scenario, the latter is the best approach as it allows for understanding what functions are running in the system, their execution conditions, and call relationships. This information can identify the most frequently called (hottest) functions and the most time-consuming ones. Understandably, this method incurs a significant performance loss due to the frequency and magnitude of function calls, especially in complex systems.
An alternative approach involves approximating the above effect by sampling call stacks. Shorter sampling intervals more closely approximate real call relationships and durations, but they can’t be too short, as obtaining stack operations itself becomes a load due to increased frequency. This method, known as a Profiler in the industry, is the basis for most programming language Profiler tools.
Static code collection is the most primitive method. It’s straightforward to implement but requires recompiling and reinstalling the program each time new content is added. If the information you need to diagnose a problem isn’t available, you have no choice but to repeat the entire process. A more advanced approach is to pre-install data collection points at all potential areas of interest, and use dynamic switches to control their output. This technique balances performance impacts and allows dynamic enabling of logs as needed, albeit at a high cost.
Dynamic tracing technology has always been available but is often considered the “holy grail” in the debugging and tracing field due to its steep learning curve. It demands a deep understanding of low-level technologies, especially in areas like compilation, ELF format, the kernel, and programming languages associated with pre-set probes and dynamic tracing. Indeed, dynamic tracing even has its own set of programming languages to cater to the dynamic implementation needs of developers. This approach balances performance and flexibility and enables dynamic retrieval of desired information even in live versions.
In Android application development and system-level development, dynamic tracing is rarely used and is occasionally employed in kernel development. Typically, only specialized performance analysts might utilize these tools. Two critical elements of dynamic tracing are probes and dynamic languages. The program’s execution permission must be handed over to the dynamic tracing framework at specific probe points during runtime. The logic executed by the framework is written by developers using dynamic languages.
Therefore, your program must first have probes. Linux kernel and other frameworks have embedded corresponding probe points, but Android application layers lack these. Currently, dynamic frameworks like eBPF on Android are mainly used by kernel developers.
Unconditional capture is straightforward: data is continuously captured after triggering, regardless of any conditions. The drawback is that when the observed object generates a large volume of data, it could significantly impact the system. In such cases, reducing the volume of data captured can mitigate the impact, striking a balance between meeting requirements and minimizing performance loss.
Conditional capture is often employed in scenarios where anomalies can be identified. For instance, capturing logs is triggered when a specific observed value exceeds a pre-set threshold and continues for a certain duration or until another threshold is reached. This method is a slight improvement over unconditional capture as it only impacts the system when an issue arises, leaving it unaffected at other times. However, it requires the capability to identify anomalies, and those anomalies should not necessitate historical data preceding the occurrence. Lowering the threshold can increase the probability of triggering data capture, leading to the same issues faced with unconditional capture, and requiring a balance of performance loss.
Continuous disk writing involves storing all data captured during the entire data capture process, which can strain storage resources. If the trigger point, such as an anomaly, can be identified, selective disk writing becomes an option. To ensure the validity of historical data, logs are temporarily stored in a RingBuffer and only written to disk upon receiving a disk write command. This method balances performance and storage pressure but at the cost of runtime memory consumption and the accuracy of the trigger.
As the complexity of problem analysis increases, especially with the need to address performance issues arising from the interactions among multiple modules, data visualization analysis methods have emerged. These methods visualize events on respective lanes with time as the horizontal axis, facilitating a clear understanding of when specific events occur and their interactions with other systems. In Android, tools like Systrace/Perfetto and, earlier, KernelShark, are fundamentally of this type. The “Trace Type” mentioned in “Data Types” often employs this kind of visualization.
Systrace’s visualization framework is built on a Chrome subproject called Catapult. The Trace Event Format outlines the data formats supported by Catapult. If you have Trace type data, you can use this framework for data visualization. AOSP build systems and the Android app compilation process also output corresponding Trace files, with visualization effects based on Catapult.
For extensive data analysis, formatting data and converting it into two-dimensional data tables enables efficient query operations using SQL. In the server domain, technology stacks like ELK offer flexible formatted search and statistical functions. With databases and Python, you can even create an automated data diagnostic toolchain.
From the discussion above, it’s evident that text analysis and database analysis serve different analytical purposes. Text analysis is sufficient for evaluating the time consumption of a single module, visualization tools are needed for interactions among multiple systems, and SQL tools are required for complex database analysis. Regardless of the analysis method, the core is data analysis. In practice, we often convert data using other tools to support different analysis methods, such as transitioning from text analysis to database analysis.
Choosing the right analysis method according to your objectives can make your work highly efficient.
For Android developers, Google provides several essential performance analysis tools to assist both system and app developers in optimizing their programs.
Based on practical experience, the most commonly used tools are Systrace, Perfetto, and the Profiler tool in Android Studio. Only after identifying the main bottlenecks using these tools would you need to resort to other domain-specific tools. Therefore, we will focus on the application scenarios, advantages, and basic usage of these three tools. For a horizontal comparison between the tools, please refer to the content in the next chapter, “Comprehensive Comparison.”
Systrace is a visualization analysis tool for the Trace type and represents the first generation of system-level performance analysis tools. It supports all the features facilitated by the Trace type. Before the emergence of Perfetto, Systrace was essentially the only performance analysis tool available. It presents the operating information of both the Android system and apps graphically. Compared to logs, Systrace’s graphical representation is more intuitive; and compared to TraceView, the performance overhead of capturing Systrace can be virtually ignored, minimizing the impact of the observer effect to the greatest extent.
Systrace embeds information similar to logs, known as TracePoints (essentially Ftrace information), at key system operations (such as Touch operations, Power button actions, sliding operations, etc.), system mechanisms (including input distribution, View drawing, inter-process communication, process management mechanisms, etc.), and software and hardware information (covering CPU frequency information, CPU scheduling information, disk information, memory information, etc.). These TracePoints depict the execution time of core operation processes and the values of certain variables. The Android system collects these TracePoints scattered across various processes and writes them into a file. After exporting this file, Systrace analyzes the information from these TracePoints to obtain the system’s operational information over a specific period.
In the Android system, some essential modules have default inserted TracePoints, classified by TraceTag, with information sources as follows:
android.os.Trace
class.android.os.Trace
class.Consequently, Systrace can collect and display all information from both upper and lower layers of Android. For Android developers, Systrace’s most significant benefit is turning the entire Android system’s operational status from a black box into a white box. Its global nature and visualization make Systrace the first choice for Android developers when analyzing complex performance issues.
The parsed Systrace, rich in system information, is naturally suited for analyzing the performance issues of both Android Apps and the Android system. Android app developers, system developers, and kernel developers can all use Systrace to diagnose performance problems.
From a Technical Perspective:
Systrace can cover major categories involved in performance, such as response speed, frame drops or janks, and ANR (Application Not Responding) issues.
From a User Perspective:
Systrace can analyze various performance issues encountered by users, including but not limited to:
When encountering the above problems, various methods can be employed to capture Systrace. The parsed file can then be opened in Chrome for analysis.
The ability to trace and visualize these issues makes Systrace an invaluable tool for developers aiming to optimize the performance of Android applications and the system itself. By analyzing the data collected, developers can identify bottlenecks and problematic areas, formulate solutions, and effectively improve the performance and responsiveness of apps and the Android operating system.
Google initiated the first submission in 2017, and over the next four years (up until Dec 2021), over 100 developers made close to 37,000 commits. There are PRs and merges almost daily, marking it as an exceptionally active project. Besides its powerful features, its ambition is significant. The official website claims it to be the next-generation cross-platform tool for Trace/Metric data capture and analysis. Its application is also quite extensive; apart from the Perfetto website, Windows Performance Tool, Android Studio, and Huawei’s GraphicProfiler also support the visualization and analysis of Perfetto data. We believe Google will continue investing resources in the Perfetto project. It is poised to be the next-generation performance analysis tool, wholly replacing Systrace.
The most significant improvement of Perfetto over Systrace is its ability to support long-duration data capture. This is made possible by a service that runs in the background, enabling the encoding of collected data using Protobuf and saving it to disk. From the perspective of data sourcing, the core principle is consistent with Systrace, both based on the Linux kernel’s Ftrace mechanism for recording key events in both user and kernel spaces (ATRACE, CPU scheduling). Perfetto supports all functionalities provided by Systrace, hence the anticipation of Systrace being replaced by Perfetto entirely.
Perfetto’s support for data types, acquisition methods, and analysis approaches is unprecedentedly comprehensive. It supports virtually all types and methods. ATRACE enables the support for Trace type, a customizable node reading mechanism supports Metric type, and in UserDebug versions, Log type support is realized by obtaining Logd data.
You can manually trigger capture and termination via the Perfetto.dev webpage or command-line tools, initiate long-duration capture via the developer options in the settings, or dynamically start data capture via the Perfetto Trigger API integrated within the framework. This covers all scenarios one might encounter in a project.
In terms of data analysis, Perfetto offers a data visualization analysis webpage similar to Systrace, but with an entirely different underlying implementation. The biggest advantage is its ability to render ultra-large files, a feat Systrace cannot achieve (it might crash or become extremely laggy with files over 300M). On this visualization webpage, one can view various processed data, execute SQL query commands, and even view logcat content. Perfetto Trace files can be converted into SQLite-based database files, enabling on-the-spot SQL execution or running pre-written SQL scripts. You can even import it into data science tool stacks like Jupyter to share your analysis strategies with colleagues.
For example, if you want to calculate the total CPU consumption of the SurfaceFlinger thread, or identify which threads are running on large cores, etc., you can collaborate with domain experts to translate their experiences into SQL commands. If that still does not meet your requirements, Perfetto also offers a Python API, converting data into DataFrame format, enabling virtually any desired data analysis effect.
With all these offerings, developers have abundant aspects to explore. From our team’s practical experience, it can almost cover every aspect from feature development, function testing, CI/CD, to online monitoring and expert systems. In the subsequent series of articles on our planet, we will focus on Perfetto’s powerful features and the expert systems developed based on it, aiding you in pinpointing performance bottlenecks with a single click.
Perfetto has become the primary tool used in performance analysis, with Systrace’s usage dwindling. Hence, the tool you should master first is Perfetto, learning its usage and the metrics it provides.
However, Perfetto has its boundaries. Although it offers high flexibility, it essentially remains a static data collector and not a dynamic tracing tool, fundamentally different from eBPF. The runtime cost is relatively high because it involves converting Ftrace data to Perfetto data on the mobile device. Lastly, it doesn’t offer text analysis methods; additional analyses can only be performed via webpage visualization or operating SQLite. In summary, Perfetto is powerful, covering almost every aspect of observability technology, but also has a relatively high learning curve. The knowledge points worth exploring and learning are plentiful, and we will focus on this part in our upcoming articles.
The integrated development environment for Android application development (officially recommended) is Android Studio (previously it was Eclipse, but that has been phased out). It naturally needs to integrate development and performance optimization. Fortunately, with the iterations and evolution of Android Studio, it now has its own performance analysis tool, Android Profiler. This is a collective tool integrating several performance analysis utilities, allowing developers to optimize performance without downloading additional tools while developing applications in Android Studio.
Currently, Android Studio Profiler has integrated four types of performance analysis tools: CPU, Memory, Network, and Battery. The CPU-related performance analysis tool is the CPU Profiler, the star of this chapter. It integrates all CPU-related performance analysis tools, allowing developers to choose based on their needs. Many people might know that Google has developed some independent CPU performance analysis tools, like Perfetto, Simpleperf, and Java Method Trace. CPU Profiler does not reinvent the wheel; it gathers data from these known tools and parses it into a desired style, presenting it through a unified interface.
CPU Profiler integrates performance analysis tools: Perfetto, Simpleperf, and Java Method Trace. It naturally possesses all or part of the functionalities of these tools, such as:
Application performance issues are mainly divided into two categories: slow response and lack of smoothness.
How to use CPU Profiler in these scenarios? The basic approach is to capture a System Trace first, analyze and locate the issue with System Trace. If the issue can’t be pinpointed, further analysis and location should be done with Java Method Trace or C/C++ Function Trace.
Taking an extremely poor-performing application as an example, suppose Systrace TracePoint is inserted at the system’s critical positions and the code is unfamiliar. How do you identify the performance bottleneck? First, run the application and record a System Trace with CPU Profiler (the tool usage will be introduced in later articles), as shown below:
From the above Trace, it’s evident that the onDrawFrame operation in the egl_core thread is time-consuming. If the issue isn’t apparent, it’s advised to export it to https://ui.perfetto.dev/ for further analysis. By looking into the source code, we find that onDrawFrame is the duration of the Java function onDrawFrame. To analyze the duration of the Java function, we need to record a Java Method Trace, as follows:
From the above Trace, it’s easy to see that a native function called Utils.onDraw is time-consuming. Because it involves C/C++ code, another C/C++ Function Trace needs to be recorded for further analysis, as shown below:
It becomes clear that the code executed a sleep function within the native Java_com_gl_shader_Utils_onDraw, pinpointing the culprit for the poor performance!
The greatest advantage of CPU Profiler in AS is the integration of various sub-tools, enabling all operations in one place. It’s incredibly convenient for application developers. However, system developers might not be so lucky.
Tool Name | Application Scenario | Data Type | Data Acquisition Method | Analysis Method |
---|---|---|---|---|
Systrace | Android System & App Performance Analysis | Trace Type | Unconditional Capture, Continuous Logging | Visual Analysis |
Perfetto | Android System & App Performance Analysis | Metric Type, Trace Type | Unconditional Capture, Continuous Logging | Visual Analysis, Database Analysis |
AS Profiler | Android System & App Performance Analysis | Trace Type | Unconditional Capture, Continuous Logging | Visual Analysis |
SimplePerf | Java/C++ Function Execution Time Analysis, PMU Counters | Trace Type | Unconditional Capture, Continuous Logging | Visual Analysis, Text Analysis |
Snapdragon Profiler Tools & Resources | Primarily for Qualcomm GPU Performance Analyzer | Trace Type, Metric Type | Unconditional Capture, Continuous Logging | Visual Analysis |
Mali Graphics Debugger | ARM GPU Analyzer (for MTK, Kirin chips) | Trace Type, Metric Type | Unconditional Capture, Continuous Logging | Visual Analysis |
Android Log/dumpsys | Comprehensive Analysis | Log Type | Conditional Capture, Continuous Capture but not Logging | Text Analysis |
AGI (Android GPU Inspector) | Android GPU Analyzer | Trace Type, Metric Type | Unconditional Capture, Continuous Logging | Visual Analysis |
eBPF | Dynamic Tracing of Linux Kernel Behavior | Metric Type | Dynamic Tracing, Conditional Capture, Continuous Capture but not Logging | Text Analysis |
FTrace | Linux Kernel Tracing | Log Type | Static Code, Conditional Capture, Continuous Capture but not Logging | Text Analysis |
Technical revolutions and improvements are often reflected at the “instruments” level. The development direction of tools by the Linux community and Google is towards enhancing the integration of tools so that necessary information can be easily found in one place, or towards the collection of more information. In summary, the development trajectory at the instruments level is traceable and developmental rules can be summarized. We need to accurately understand their capabilities and application scenarios during rapid iterations of tools, aiming to improve problem-solving efficiency rather than spending time learning new tools.
The “techniques” level depends on specific business knowledge, understanding how a frame is rendered, how the CPU selects processes for scheduling, how IO is dispatched, etc. Only with an understanding of business knowledge can one choose the right tools and correctly interpret the information provided by these tools. With rich experience, sometimes you can spot clues even without looking at the detailed information provided by tools. This is a capability that arises when your business knowledge is enriched to a certain extent, and your brain forms complex associative information, elevating you above the tools.
At the “philosophy” level, considerations are about the nature of the problem that needs to be solved. What is the essence of the problem? What extent should be achieved, and what cost should be incurred to achieve what effect? For solving a problem, which path has the highest “input-output ratio”? What is the overall strategy? To accomplish something, what should be done first and what should be done next, and what is the logical dependency relationship?
In subsequent articles, explanations will be provided in the “instruments, techniques, philosophy” manner for a technology or a feature. We aim not only to let you learn a knowledge point but also to stimulate your ability to extrapolate. When faced with similar tools or problems, or even completely different systems, you can handle them with ease. Firmly grasping the essence, you can choose the appropriate tools or information through evaluating the “input-output ratio” and solve problems efficiently.
An individual can move faster, a group can go further.
]]>Paul Graham 在其著作 <黑客与画家> 中断言:“不同语言的执行效率差距正变得越来越大,所以性能分析器(profiler)将变得越来越重要。目前,性能分析并没有受到重视。许多人好像仍然相信,程序运行速度提升的关键在于开发出能够生成更快速代码的编译器。代码效率与机器性能的差距正在不断加大,我们将会越来越清楚地看到,应用软件运行速度提升的关键在于有一个好的性能分析器帮助指导程序开发。”
by Paul Graham 黑客与画家
谷歌搜索 「Android 优化工具」,你会找到很多与此相关的内容。他们的问题在于要么是内容高度重复、要么是直接讲使用方法,很少会给你介绍整体性的架构,一不小心就会让人会种「一个工具搞定一切」的错误认知。以笔者团队的多年经验来看,在性能分析领域这种银弹级别的工具是不存在的。工具在发展,老问题会以新的方式变样出现,不掌握核心逻辑的话始终会让你浮于技术的表面。
本文首先系统性的梳理性能分析中的可观测性技术,它涵盖数据类型、抓取方法以及分析方法等三部分内容,之后是介绍谷歌提供的「三大件」分析工具。目的是想让你了解不变的理论性的知识,以及与之对应的在安卓环境中可用的工具,这些可以让你少走一些弯路,直接复用前辈们的经验。
需要特别说明的是,对于性能优化肯定不止有这三个工具可用,但这个三个工具是我们平时用到的「第一手工具」。进行进一步分析之前,你都需要依赖这三个工具进行瓶颈定位,之后才应不同领域特性选择对应的工具进行下钻分析。
相信你不止一次被同事、被老板问到过类似的问题。最原始的想法应该是,首先是拿到相关的日志进行逐个分析。根据以往经验,通过查找关键字寻找蛛丝马迹。如果没有想看的信息,那就加上日志尝试本地复现。费时费力不说,也还费研发资源。但你有没有想过行业里有没有更高效的方法?可以提高一个数量级的那种,把我们的时间花在问题解决上而不是无聊的重复性体力活儿上?
答案当然是有的(否则就不会有这篇文章了),我们称他为可观测性技术。
计算机行业发展至今,计算机前辈们捣鼓出了所谓的「可观测性技术」的类别。它研究的是通过工具,来观测复杂系统的运行细节,内容越细越好。 移动操作系统之前是由嵌入式发展而来的,现在的中高端安卓手机算力都能赶得上二十几年前的一个主机的算力,在此算力基础上所带来的软件复杂度也是非常巨大的。
如果你的程序部署了一个精心设计且运行良好的可观测性技术,可以大大加快研发软件的效率,因为即使我们使用了各种各样的前置性静态代码检测、人工代码审查,也无法 100% 拦截软件的问题。只有在真实环境里运行之后才知道是否真正发生了问题,即使这个环境可能是一个你的自动化测试用例。即使这样,你还需要翻阅你的日志,重读代码来找出问题。出于这些原因,每个工程团队都需要有一个功能完备的可观测性工具作为他们的基础设施之一。
可观测性技术是一个系统性工程,它能够让你更深入的了解软件里发生的事情。可用于了解软件系统内部运行过程(特别是对于业务逻辑或者交互关系复杂的系统)、排查问题甚至通过寻找瓶颈点优化程序本身。对于复杂的系统来说,你通过阅读代码来了解整个运行过程其实是很困难的事情,更高效的方法就是借助此类工具,以最直观的的方式获取软件运行的状态。
下面将从 数据类型、数据获取方法、分析方法 这三个主题来帮助你了解可观测性技术。
日志的形式可能是键值对(key=Value),JSON、CSV,关系型数据库或者其他任何格式。其次我们通过日志还原出系统当时运行的整个状态,目的是为了解决某个问题,观察某个模块的运行方式,甚至刻画系统使用者的行为模式。在可观测性技术上把日志类型分类为 Log 类型、Metric 类型,以及 Trace 类型。
Log 是最朴素的数据记录方式,一般记录了什么模块在几点发生了什么事情,日志等级是警告还是错误。 绝大部分系统,不管是嵌入式设备还是汽车上的计算机,他们所使用的日志形式几乎都是这种形式。这是最简单,最直接也最好实现的一种方式。几乎所有的 Log 类型是通过 string 类型的方式存储,数据呈现形式是一条一条的文本数据。Log 是最基本的类型,因此通过转换,可以将 Log 类型转换成 Metric 或者 Trace 类型,当然成本就是转换的过程,当数据量非常巨大的时候这可能会成为瓶颈。
为了标识出不同的日志类型等级,一般使用错误、警告、调试等级别来划分日志等级。显然,错误类型的是你首要关注的日志等级。不过实践中也不会严格按照这种方式划分,因为很多工程师不会严格区分他们之间的差异,这可能是他们的工程开发环境中不太会对不同等级的日志进行分类分析有关。总之,你可以根据你的目的,将 Log 类型进行等级划分,它就像一个索引一样,可以进一步可以提高分析问题、定位目标信息的效率。
Metric 类型相比 Log 类型使用目的上更为聚焦,它记录的是某个维度上数值的变化。知识点是「维度」与「数值」的变化。维度可能是 CPU 使用率、CPU Cluster 运行频率,或者上下文切换次数。数值变化既可以是采样时候的瞬时值(成为快照型)、与前一次采样时的差值(增或减)、或者某个时段区间的统计聚合值。实践中经常会使用统计值,比如我想看问题发生时刻前 5 分钟的 CPU 平均使用量。这时候需要将这五分钟内的所有数值做算数平均计算,或者是加权平均(如: 离案发点越近的样本它的权重就越高)。Log 类型当然可以实现 Metric 类型的效果,但是操作起来非常麻烦而且其性能损耗可能也不小。
聚合是非常有用的工具,因为人不可能逐个分析所有的 Metric 值,因此借助聚合的方式判断是否出了问题之后再进行详细的分析是更为经济高效的方法。
Metric 类型的另外一个好处是它的内容格式是比较固定的,因此可以通过预编码的方式进行数据存储,空间的利用率会更紧凑进而占用的磁盘空间就更少。最简单的应用就是数据格式的存储上,如果使用 Log 类型,一般采用的是 ASCII 编码,而 Metric 使用的是整数或者浮点等固定 byte 数的数据,当存储较大数值时显然 ASCII 编码需要的字节数会多于数字型数据,并且在进行数据处理的时候你可以直接使用 Metric 数据,而不需要把 Log 的 ASCII 转换成数字型后再做转换。
除了是具体的数值之外,也可以存储枚举值(某种程度上它的本质就是数值)。不同的枚举值代表不同的意义,可能是开和关、可能是不同的事件类型。
Trace 类型标识了事件发生的时间、名称、耗时。多个事件通过关系,标识出了是父与子还是兄弟。当分析多个线程间复杂的调用关系时 Trace 类型是最方便的数据分析方式。
Trace 类型特别适用于 Android 应用与系统级的分析场景,因为用它可以诊断:
Android 的应用程序运行环境的设计中,一个应用程序是无法独自完成所有的功能的,它需要跟 SystemServer 有大量的交互才能完成它的很多功能。与 SystemServer 间的通讯是通过 Binder 完成,它的通讯方式后面的文章再详细介绍,到目前为止你只需要知道它的调用关系是跨进程调用即可。这需要本端与远端的数据才能准确还原出调用关系,Trace 类型是完成这种信息记录的最佳方式。
Trace 类型可以由你手动添加开始与结束点,在一个函数里可以添加多个这种区间。通过预编译技术或者编程语言的特性,在函数的开头与结尾里自动插桩 Trace 区间。理想情况下后者是最好的方案,因为我们能知道系统中运行的所有的函数是哪些、执行情况与调用关系是什么。可以拿这些信息统计出调用次数最多(最热点)的函数是什么,最耗时的函数又是什么。可想而知这种方法带来的性能损耗非常大,因为函数调用的频次跟量级是非常大的,越是复杂的系统量级就越大。
因此有一种迂回的方法,那就通过采样获取调用栈的方式近似拟合上面的效果。采样间隔越短,就越能拟合真实的调用关系与耗时,但间隔也不能太小因为取堆栈的操作本身的负载就会变高因为次数变多了。这种方法,业界管他叫 Profiler,你所见过的绝大部分编程语言的 Profiler 工具都是基于这个原理实现的。
静态代码的采集方式是最原始的方式,优点是实现简单缺点是每次新增内容的时候需要重新编译、安装程序。当遇到问题之后你想看的信息恰好没有的话,就没有任何办法进一步定位问题,只能重新再来一遍整个过程。更进一步的做法是预先把所有可能需要的地方上加入数据获取点,通过动态判断开关的方式选择是否输出,这既可以控制影响性能又能够在需要日志的时候可以动态打开,只不过这种方法的成本非常高。
动态跟踪技术其实一直都存在,只是它的学习成本比较高,被誉为调试跟踪领域里的屠龙刀。它需要你懂比较底层的技术,特别是编译、ELF 格式、内核、以及熟悉代码中的预设的探针、动态跟踪所对应的编程语言。对,你没看错,这种技术甚至还有自己的一套编程语言用于「动态」的实现开发者需求。这种方式兼具性能、灵活性,甚至线上版本里遇到异常后可以动态查看你想看的信息。
Android 应用开发、系统级开发中用的比较少,内核开发中偶尔会用一些。只有专业、专职的性能分析人员才可能会用上这类工具。它有两个关键点,探针与动态语言,程序运行过程中需要有对应的探针点将程序执行权限交接到动态跟踪框架,框架执行的逻辑是开发者使用动态语言来编写的逻辑。
所以,你的程序里首先是要有探针,好在 Linux 内核等框架埋好了对应的探针点,但是 android 应用层是没有现成的。所以目前 Android 上能用动态框架,如 eBPF 基本都是内核开发者在使用。
无条件式抓取比较好理解,触发抓取之后不管发生任何事情,都会持续抓取数据。缺点是被观测对象产生的数据量非常大的时候可能会对系统造成比较大的影响,这种时候只能通过降低数据量的方式来缓解。需要做到既能满足需求,性能损失又不能太大。
有条件式抓取经常用在可以识别出的异常的场景里。比如当系统的某个观测值超过了预先设定的阈值时,此时触发抓取日志并且持续一段时间或者达到另外一种阈值之后结束抓取。这相比于前面一个方法稍微进步了一些,仅在出问题的时候对系统有影响,其他时候没有任何影响点。但它需要你能够识别出异常,并且这种异常是不需要异常发生之前的历史数据。当然你可以通过降低阈值来更容易达到触发点,这可能会提高触发数据抓取的概率,这时候会遇到前面介绍的无条件式抓取遇到的同样的问题,需要平衡性能损失。
持续落盘是存储整个数据抓取过程中的所有数据,代价是存储的压力。如果能知道触发点,比如能够检测到异常点,这时候可以选择性的落盘。为了保证历史数据的有效性,因此把日志先暂存储到 RingBuffer 中,只有接受到落盘指令后再进行落盘存储。这种方式兼顾了性能与存储压力,但成本是运行时内存损耗与触发器的准确性。
随着问题分析的复杂化,出现了要解决多个模块间交互的性能问题需求,业界就出现了以时间为横轴把对应事件放到各自泳道上的数据可视化分析方法,可以方便的看到所关心事件什么时候发生、与其他系统的交互信息等等。在 Android 里我们常用的 Systrace/Perfetto 以及更早之前的 KernelShark 等工具本质上都是这一类工具。在「数据类型」提到的 「Trace 类型」,经常采用这种可视化分析方法。
Systrace 的可视化框架是基于 Chrome 的一个叫 Catapult 的子项目构建。Trace Event Format 讲述了 Catapult 所支持的数据格式,如果你有 Trace 类型的数据,完全可以使用此框架来展示可视化数据。AOSP 编译系统,安卓应用的编译过程,也都有相应的 Trace 文件输出,它们也都基于 Catapult 实现了可视化效果。
面对大量数据分析的分析,通过对数据进行格式化,把他们转换成二维数据表,借助 SQL 语言可实现高效的查询操作。在服务器领域中 ELK 等技术栈可以实现更为灵活的格式化搜索与统计功能。借助数据库与 Python,你甚至可以实现一套自动化数据诊断工具链。
从上面的讨论可知,从文本分析到数据库分析他们要面对的分析目的是不一样的。单纯的看一个模块的耗时用文本分析就够用了,多个系统间的交互那就要用可视化工具,复杂的数据库分析就要用到 SQL 的工具。无论哪种分析方式,本质上都是针对数据的分析,在实战中我们经常会通过其他工具对数据进行转换以支持不同的分析方式,比如从文本分析方式改成数据库分析方式。
根据自己的目的,选择合适的分析方式才会让你的工作事倍功半。
对于 Android 开发者来说,Google 提供了几个非常重要的性能分析工具,帮助系统开发者、应用开发者来优化他们的程序。
从实践经验来看最常用的工具有 Systrace,Perfetto 与 Android Studio 中的 Profiler 工具。通过他们定位出主要瓶颈之后,你才需要用到其他领域相关工具。因此,会重点介绍这三个工具的应用场景,它的优点以及基本的使用方法。 工具之间的横向对比,请参考下一个「综合对比」这一章节的内容。
Systrace 是 Trace 类型的可视化分析工具,是第一代系统级性能分析工具。Trace 类型所支持的功能它都有支持。在 Perfetto 出现之前,基本上是唯一的性能分析工具,它将 Android 系统和 App 的运行信息以图形化的方式展示出来,与 Log 相比,Systrace 的图像化方式更为直观;与 TraceView 相比,抓取 Systrace 时候的性能开销基本可以忽略,最大程度地减少观察者效应带来的影响。
在系统的一些关键操作(比如 Touch 操作、Power 按钮、滑动操作等)、系统机制(input 分发、View 绘制、进程间通信、进程管理机制等)、软硬件信息(CPU 频率信息、CPU 调度信息、磁盘信息、内存信息等)的关键流程上,插入类似 Log 的信息,我们称之为 TracePoint(本质是 Ftrace 信息),通过这些 TracePoint 来展示一个核心操作过程的执行时间、某些变量的值等信息。然后 Android 系统把这些散布在各个进程中的 TracePoint 收集起来,写入到一个文件中。导出这个文件后,Systrace 通过解析这些 TracePoint 的信息,得到一段时间内整个系统的运行信息。
Android 系统中,一些重要的模块都已经默认插入了一些 TracePoint,通过 TraceTag 来分类,其中信息来源如下
这样 Systrace 就可以把 Android 上下层的所有信息都收集起来并集中展示,对于 Android 开发者来说,Systrace 最大的作用就是把整个 Android 系统的运行状态,从黑盒变成了白盒。全局性和可视化使得 Systrace 成为 Android 开发者在分析复杂的性能问题的时候的首选。
解析后的 Systrace 由于有大量的系统信息,天然适合分析 Android App 和 Android 系统的性能问题, Android 的 App 开发者、系统开发者、Kernel 开发者都可以使用 Systrace 来分析性能问题。
在遇到上述问题后,可以使用多种方式抓取 Systrace ,将解析后的文件在 Chrome 打开,然后就可以进行分析
谷歌在 2017 年开始了第一笔提交,随后的 4 年(截止到 2021.12)内总共有 100 多位开发者提交了近 3.7W 笔提交,几乎每天都有 PR 与 Merge 操作,是一个相当活跃的项目。 除了功能强大之外其野心也非常大,官网上号称它是下一代面向可跨平台的 Trace/Metric 数据抓取与分析工具。应用也比较广泛,除了 Perfetto 网站,Windows Performance Tool 与 Android Studio,以及华为的 GraphicProfiler 也支持 Perfetto 数据的可视化与分析。 我们相信谷歌还会持续投入资源到 Perfetto 项目,可以说它应该就是下一代性能分析工具了,会完全取代 Systrace。
Perfetto 相比 Systrace 最大的改进是可以支持长时间数据抓取,这是得益于它有一个可在后台运行的服务,通过它实现了对收集上来的数据进行 Protobuf 的编码并存盘。从数据来源来看,核心原理与 Systrace 是一致的,也都是基于 Linux 内核的 Ftrace 机制实现了用户空间与内核空间关键事件的记录(ATRACE、CPU 调度)。Systrace 提供的功能 Perfetto 都支持,由此才说 Systrace 最终会被 Perfetto 替代。
Perfetto 所支持的数据类型、获取方法,以及分析方式上看也是前所未有的全面,它几乎支持所有的类型与方法。数据类型上通过 ATRACE 实现了 Trace 类型支持,通过可定制的节点读取机制实现了 Metric 类型的支持,在 UserDebug 版本上通过获取 Logd 数据实现了 Log 类型的支持。
你可以通过 Perfetto.dev 网页、命令行工具手动触发抓取与结束,通过设置中的开发者选项触发长时间抓取,甚至你可以通过框架中提供的 Perfetto Trigger API 来动态开启数据抓取,基本上涵盖了我们在项目上能遇到的所有的情境。
在数据分析层面,Perfetto 提供了类似 Systrace 操作的数据可视化分析网页,但底层实现机制完全不同,最大的好处是可以支持超大文件的渲染,这是 Systrace 做不到的(超过 300M 以上时可能会崩溃、可能会超卡)。在这个可视化网页上,可以看到各种二次处理的数据、可以执行 SQL 查询命令、甚至还可以看到 logcat 的内容。Perfetto Trace 文件可以转换成基于 SQLite 的数据库文件,既可以现场敲 SQL 也可以把已经写好的 SQL 形成执行文件。甚至你可以把他导入到 Jupyter 等数据科学工具栈,将你的分析思路分享给其他伙伴。
比如你想要计算 SurfaceFlinger 线程消耗 CPU 的总量,或者运行在大核中的线程都有哪一些等等,可以与领域专家合作,把他们的经验转成 SQL 指令。如果这个还不满足你的需求, Perfetto 也提供了 Python API,将数据导出成 DataFrame 格式近乎可以实现任意你想要的数据分析效果。
这一套下来供开发者可挖掘的点就非常多了,从笔者团队的实践来看,他几乎可以覆盖从功能开发、功能测试、CI/CD 以及线上监控、专家系统等方方面面。本星球的后续系列文章中,也会重点介绍 Perfetto 的强大功能与基于它开发的专家系统,可以帮助你「一键解答」性能瓶颈。
性能分析首要用到的工具就是 Perfetto,使用 Systrace 的场景是越来越少了。所以,你首要掌握的工具应该是 Perfetto,学习它的用法以及它提供的指标。
不过 Perfetto 也有一些边界,首先它虽然提供了较高的灵活性但本质上还是静态数据收集器,不是动态跟踪工具,跟 eBPF 还是有本质上的差异。其次运行时成本比较高,因为涉及到在手机中实现 Ftrace 数据到 Perfetto 数据的转换。最后他不提供文本分析方式,只能通过网页可视化或者操作 SQLite 来进行额外的分析了。综合来看 Perfetto 是功能强大,几乎涵盖了可观测性技术的方方面面,但是使用门槛也比较高。值得挖掘与学习的知识点比较多,我们后续的文章中也会重点安排此部分的内容。
Android 的应用开发集成环境(官方推荐)是 Android Studio (之前是Eclipse,不过已经淘汰了) ,它自然而然也需要把开发和性能调优集成一起。非常幸运的是,随着 Android Studio 的迭代、演进,到目前,Android Studio 有了自己的性能分析工具 Android Profiler,它是一个集合体,集成了多种性能分析工具于一体,让开发者可以在 Android Studio 做开发应用,也不用再下载其它工具就能让能做性能调优工作。
目前 Android Studio Profiler 已经集成了 4 类性能分析工具: CPU、Memory、Network、Battery,其中 CPU 相关性能分析工具为 CPU Profiler,也是本章的主角,它把 CPU 相关的性能分析工具都集成在了一起,开发者可以根据自己需求来选择使用哪一个。可能很多人都知道,谷歌已经开发了一些独立的 CPU 性能分析工具,如 Perfetto、Simpleperf、Java Method Trace 等,现在又出来一个 CPU Profiler,显然不可能去重复造轮子,CPU Profiler 目前做法就是:从这些已知的工具中获取数据,然后把数据解析成自己想要的样式,通过统一的界面展示出来。
CPU Profiler 集成了性能分析工具:Perfetto、Simpleperf、Java Method Trace,它自然而然具备了这些工具的全部或部分功能,如下:
应用的性能问题主要分为两类:响应慢、不流畅。
CPU Profiler 在这些场景中要如何使用呢?基本的思路是:首先就要抓 System Trace,先用System Trace 分析、定位问题,如果不能定位到问题,再借助 Java Method Trace 或 C/C++ Function Trace 进一步分析定位。
以一个性能极差的应用为例,在系统的关键位置插了 Systrace TracePoint,假设对代码不熟悉,那要怎么找到性能瓶颈呢?我们先把应用跑起来,通过 CPU Profiler 录制一个 System Trace (后面文章会介绍工具的使用方法)如下:
通过上面 Trace 可以知道是在 egl_core 线程中的 onDrawFrame 操作耗时,如果发现不了问题,建议导出到 https://ui.perfetto.dev/ 进一步分析,可以查找源代码看看 onDrawFrame 是什么东西, 我们通过查找发现 onDrawFrame 是 Java 函数 onDrawFrame 的耗时,要分析 Java 函数耗时情况,我们要录制一个 Java Method Trace,如下:
通过上面 Trace 很容易发现是一个叫做 Utils.onDraw 的 native 函数耗时,因为涉及到C/C++ 代码,所以要再录制一个 C/C++ Function Trace 进一步分析,如下:
可以发现在 native 的 Java_com_gl_shader_Utils_onDraw 中代码执行了 sleep,它就是导致了性能低下的罪魁祸首!
AS 中的 CPU Profiler 最大优势是集成了各种子工具,在一个地方就能操作一切,对应用开发者来说是非常方便的,不过对系统开发者来说可能没那么幸运。
工具名称 | 应用场景 | 数据类型 | 获取方法 | 分析方式 |
---|---|---|---|---|
Systrace | Android 系统与应用性能分析 | Trace 类型 | 无条件抓取 持续落盘 | 可视化分析 |
Perfetto | Android 系统与应用性能分析 | Metric 类型 Trace 类型 | 无条件抓取 持续落盘 | 可视化分析 数据库分析 |
AS Profiler | Android 系统与应用性能分析 | Trace 类型 | 无条件抓取 持续落盘 | 可视化分析 |
SimplePerf | Java/C++ 函数执行耗时 分析 PMU 计数器 | Trace 类性 | 无条件抓取 持续落盘 | 可视化分析 文本分析 |
Snapdragon Profiler Tools & Resources | 主要是高通 GPU 性能分析器 | Trace 类型 Metric 类型 | 无条件抓取 持续落盘 | 可视化分析 |
Mali Graphics Debugger | ARM GPU 分析器(MTK、麒麟芯片) | Trace 类型 Metric 类型 | 无条件抓取 持续落盘 | 可视化分析 |
Android Log/dumpsys | 综合分析 | Log 类型 | 有条件抓取 持续抓取但不落盘 | 文本分析 |
AGI(Android GPU Inspector) | Android GPU 分析器 | Trace 类型 Metric 类型 | 无条件抓取 持续落盘 | 可视化分析 |
eBPF | Linux 内核行为动态跟踪 | Metric 类型 | 动态跟踪 有条件抓取 持续抓取但不落盘 | 文本分析 |
FTrace | Linux 内核埋点 | Log 类型 | 静态代码 有条件抓取 持续抓取但不落盘 | 文本分析 |
技术上的变革、改进更多是体现在「器」层面,Linux 社区以及谷歌所开发的工具发展方向朝着提高工具的集成化使得在一个地方可以方便查到所需的信息、或者是朝着获取更多信息的方向发展。总之,器层面他们的发展轨迹是可寻的,可总结出发展规律。 我们需要在工具快速迭代的时候准确的认识到他们能力以及应用场景,其目的是提高解决问题的效率,而不是把时间花在学习新工具上。
「术」层面依赖具体的业务知识,知道一帧是如何被渲染的、CPU 是如何选择进程调度的、IO 是如何被下发的等等。只有了解了业务知识才能正确的选择工具并正确的解读工具所提供的信息。随着经验的丰富,有时候你都不需要看到工具提供的详细信息,也可以查到蛛丝马迹,这就是当你业务知识丰富到一定程度,大脑里形成了复杂的关联性信息之后凌驾于工具之上的一种能力。
「道」层面思考的是要解决什么问题,问题的本质是什么?做到什么程度以及需要投入什么样的成本达成什么样的效果。为了解决一个问题,什么样的路径的「投入产出比」是最高的?整体打法是什么样?为了完成一件事,你首先要做什么其次是做什么,前后依赖关系的逻辑又是什么?
后续的文章中,会依照「器、术、道」方式讲解一个技术、一个功能,我们不止想让你学习到一个知识点,更想激发你举一反三的能力。遇到类似的工具或者类似的问题、更进一步是完全不同的系统,都能够从容应对。牢牢抓住本质,通过评估「投入产出比」选择合适的工具或信息,高效解决问题。
为了更好地交流与输出高质量文章,我们创建了名为 「The Performance」的知识星球,主理人是三个国内一线手机厂商性能优化方面的一线开发者,有多年性能相关领域的工作经验,提供Android 性能相关的一站式知识服务,涵盖了基础、方法论、工具使用和最宝贵的案例分析。
目前星球的内容规划如下(两个 ## 之间的是标签,相关的话题都会打上对应的标签,方便大家点击感兴趣的标签查看对应的知识)
注意: iOS 手机用户不要直接在星球里面付款,在微信界面长按图片扫描二维码加入即可,否则苹果会收取高昂的手续
一个人可以走的更快 , 一群人可以走的更远
]]>
不过在个人成长方面,甚至感觉有点退步,这让我觉得有点慌,学如逆水行舟,不进则退,2022 年是需要好好深耕的一年,希望能和看到这篇文章的同学一起进步,共勉
另外也盘点了一下知识分享相关的数据,分享了一下这方面的收入,个人新增和推荐的硬件、个人推荐的软件等,感兴趣的可以自取
今年的最大收获那必然是家里多了一个小橘子,体验了一下当爸爸的感觉,多了个小棉袄,双人行变成了三人行,到现在快十个月了,妥妥小天使。我们对小橘子要求不高,健健康康快快乐乐长大就可以了,老父亲老母亲是你坚强的后盾
博客 2021 新增了 8 篇文章(Systrace 系列总算是完结了,总共 18 篇内容,你们先看,我去准备准备 Perfetto 版本的….)
一年就写了这几篇文章,跟年初定的目标 一周一篇 差的实在是有点远,就离谱(2022 年不能再立这种 Flag 了,量力而行,一个月两篇我觉得还可以一战。
另外去年还维护了一个 Android Weekly 的知乎专栏,感兴趣的也可以订阅一下 https://www.zhihu.com/column/c_1278963991947780096)
博客 AndroidPerformance 使用了 Google Analytics 来进行统计,我看了一下 2021 和 2020 年的对比数据,还是不错的,下面是 Google Analytics 的统计数据
**访问用户数 **对比 2020 年增长了 38.9%,总的来说还是不错的(那几个明显的低谷是新年假期、五一和十一,好好过节,咱不卷)
用户最常访问的页面,还是主要以 Android Systrace 系列为主
微信公众号 AndroidPerformance 的关注数目前是 :7364,由于原创文章不多,且有很多转载,所以公众号增长比较慢,活跃的都是老用户了。微信公众号文章由于是封闭的,所以数据没有什么意义,就不贴了,希望 2022 能突破 1W 吧
微信公众号主要是微信里面阅读方便,但是对于写作的人来说就没那么友好了,主要是没法贴微信之外的超链接,这个设定也太 XX 了,把互联网搞成了局域网,无力吐槽
知乎 的关注者目前有 2W+ ,不过知乎貌似越来越不重视技术这一块了,所以也就没怎么活跃了,刷到的文章和回答大部分都是搬运工或者水军,遇到好的文章都要赶紧点赞收藏分享一键三连,后续还是刷刷即刻和 Twitter 吧,上面的真实活跃开发者还是多
掘金 技术氛围还是很浓厚的,我也经常刷,不过总感觉掘金差了点啥,又说不上来是啥… 掘金的粉丝可以忽略不计了,不过后续有技术文章还是会同步上去的(你不同步,就会有人替你同步,当然作者就换人了)
即友可以加个好友
今天听了 Happy Xiao 的播客,讲了他一年下来,通过知识分享的各个平台,每个月大概有 2500 RMB 左右的收入,所以也想了想自己去年一年在这方面的收入,想想也还蛮惨淡的
所以算下来,总收入是 1039 + 560 = 1596 。平均每个月 133,可见是真的不赚钱……用老罗的话来说,交个朋友..不过真心感谢微信打赏的小伙伴,每天早餐加个蛋就靠你们了(各位想赞赏的话,可以扫描文末的二维码,随便找一篇原创的文章打赏即可)
说到交个朋友,确实今年新加了很多技术的小伙伴,拉了四个微信群,时不时在群里水一水,在群里总的感觉是:群除我佬,时常怀疑自己是不是出现在了错的地方。说到底还是太菜,很多东西都不知道,或者一知半解。立个 Flag:今年要把基础打牢,知识体系化,争取跟上群里的大佬们的讨论
微信赞赏码,各位觉得有用,可以加个鸡腿
新年立 Flag 一般就那几样,我也没法免俗,不管怎么说先立了再说,等后续细化成每周每天的行动项,说不定就成了呢?
2021 用的最舒心的软件
2021 还是折腾了不少硬件的,觉得还不错的推荐给大家
池大已经替我说了,我感觉我手上这台闲鱼淘的 M1 版本的 Mac Mini,在 M1 Max 芯片的 MacBook Pro 面前瞬间就不香了,操作起来也变卡了……一定是库克在远程施法了
不过想要归想要,需要归需要,要理性消费(Mini 坏了的话就可以换了)
话说这个椅子也太好看了吧!(https://item.taobao.com/item.htm?id=614505490996 各位自取)
当然还有最新款的 iPhone 13 Pro Max 谁会拒绝呢?
2021 总的来说工作上不怎么理想,生活上有了小棉袄还是添加了不少乐趣,个人成长上几乎停滞,有各方面的原因,归根结底还是自己对自己的要求太低了,逆水行舟,不进则退
希望 2022 年能和看到这篇文章的各位一起努力,共勉!
]]>一个人可以走的更快 , 一群人可以走的更远
我个人的建议是:如果你是个老鸟,不建议买,这本书里面没有介绍太多原理性的东西,对于 Android 流畅性也没有一个比较全面的介绍;如果你是新手,这本书用来当做开阔视野 + 查漏补缺还可以,想更深入的了解 Android 流畅度还是差了点东西
之所以我会这么建议,是因为这本书确实没有讲太多性能或者流畅度相关的东西,也没有比较深入的原理部分,篇幅更多在讲静态代码审查、AS Profiler 的使用、App 架构、保活、网络性能优化、APK 大小优化、App 耗电等,内容也不深,浅尝辄止
简单介绍一下这本书的内容,其章节如下
从上面章节标题大家也可以看到,跟流畅性相关的内容比较少,内容相对会比较杂一些,感兴趣的可以买一本看看
如果让我写这么一本书,我肯定是写不来的,非常钦佩能出书的技术小伙伴,给作者点个赞。
不过这并不妨碍我嘴炮打个山响(I am good at it):所以我觉得如果让我来写这本书,我会加入下面这些内容,确保大家通过这本书,就可以深入理解 Android 的流畅性原理,且可以熟练使用各种工具来分析所遇到的流畅性问题
鉴于在讨论 Android 性能问题的时候,卡顿(流畅性)、响应速度、ANR 这三个性能相关的知识点通常会放到一起来讲,因为引起卡顿、响应慢、ANR 的原因类似,只不过根据重要程度,被人为分成了卡顿(流畅性)、响应慢、ANR 三种,所以我们可以定义广义上的流畅性,包含了卡顿(流畅性)、响应慢和 ANR 三种,所以如果用户反馈说手机卡顿或者 App 卡顿(流畅性),大部分情况下都是广义上的卡顿(流畅性),需要搞清楚,到底出现了哪一种问题
所以我设想的章节应该包含下面的内容
嘴炮输出完毕,万事俱备,只欠大佬来完善内容了…
讲道理目前市面上的书都有点年代了,倒是掘金社区的 Android 性能优化文章非常多,各种大厂也乐意将他们的内部工具开源,给这些热爱分享小伙伴点个赞,让我们站在巨人的肩膀上前行
我本人看过的几本书
一个人可以走的更快 , 一群人可以走的更远
本文就带大家下载和编译最新的 Android 12 代码,本地编译的代码有下面几个好处
如果大家没有下载编译 Debug 的需求,只是单纯的看代码的话,推荐使用 cs.android.com 即可。想深入了解 Android 系统的小伙伴和 Android 系统开发初学者可以看看,建议编译配置如下
由于在国内使用 Google 的官方下载站点,会有下不动的情况,有时候 .repo 都下载不下来,所以本教程是以国内的镜像站点为例子,如果你有方法可以爬墙,那么可以简单参考 官方的教程 https://source.android.google.cn/source/downloading
科大 AOSP 镜像站点地址:https://mirrors.ustc.edu.cn/help/aosp.html
下载只需要跟着下面几个步骤走即可(以下方法可以在 Ubuntu、WSL、WSL2、Mac 上运行,但是后面进行代码编译的时候,只能使用 Linux ,所以建议大家还是使用 Ubuntu 这样的 Linux 系统来进行代码的下载、编译、开发工作)
1 | mkdir ~/bin |
如果没有安装 git,先自己安装一下 git,然后执行下面的命令,填上自己的 Name 和 Email
1 | git config --global user.name "Your Name" |
比如我填的
1 | git config --global user.name "Gracker" |
在本地建立一个工作目录(名字任意,这里以 Android_12_AOSP 为例子)
1 | mkdir Android_12_AOSP |
仓库初始化有两种方式,一种是直接下载,另外一种是加 Tag,下载特定的 Tag 版本,下面会对这两种方法分别进行介绍,大家可以自己选择哪一种方式 (注意:这里的两种下载方式会影响后续的驱动下载,所以要记清楚自己使用的是哪种方式,在 驱动下载 章节选择合适的驱动)
这种方法会下载所有的代码,默认分支是 master ,不愁空间的话,直接用这种方法下载即可
1 | repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest |
这里需要注意,默认的 repo 使用的地址是 REPO_URL = ‘https://gerrit.googlesource.com/git-repo‘ ,这里我们需要修改 REPO_URL,否则会出现无法下载的情况
下载好 .repo 之后会有下面的信息
1 | ➜ Android12 repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest |
如果选择了直接下载,那么就不需要看 3.2 了
这种方法指的是只下载单个 Tag 所对应的代码,这里的 Tag 可以 查看这里 https://source.android.google.cn/setup/start/build-numbers,比如我的开发机是 Google Pixel 3 XL,我在 Tag 列表查看对应的机型都有哪些 TAG,目前 Android 12 只发布了两个,如下
对应的 Tag 分别是 android-12.0.0_r3 和 android-12.0.0_r1 ,所以下载的时候我可以制定对应的 TAG,这样的好处是下载的代码比较少,下载速度会快一些;不方便的点是更新不方便,Google 会定期发邮件告诉你哪些新的 Tag 发布了,你可以根据这个来更新代码
1 | repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-12.0.0_r3 |
上面步骤三只是下载了 .repo 文件,具体的代码还需要执行 repo sync 来进行下载。由于镜像站的限制和下载过程中可能会遇到的问题,建议大家用 -j4 来下载
1 | repo sync -j4 |
然后就开始了漫长的下载,由于下载过程中可能会出现失败的情况,你可以搞一个 sh 脚步来循环下载,一觉醒来就下载好了
1 |
|
具体方法
1 | touch repo.sh # 1. 创建 repo.sh 文件 |
代码下载完成之后,我们先不着急编译,如果要想在真机上跑,需要下载一些厂商闭源的驱动文件,这样后续编译的代码才可以跑到真机上,此处对应的 官方文档 https://source.android.google.cn/setup/build/downloading#obtaining-proprietary-binaries
上面下载代码的时候,我们提到了两种方式,直接下载和下载特定 Tag,不同的下载方式对应的驱动也不一样
直接下载的代码使用的是 master 分支,驱动程序需要在这里下载 https://developers.google.cn/android/blobs-preview
以我的 pixel 3 XL 为例,我需要下载的驱动是
点击 Link 下载两个文件,然后进行解压到代码根目录,然后执行 sh 脚本释放驱动到合适的位置,二进制文件及其对应的 makefile 将会安装在源代码树的 vendor/ 层次结构中
如果下载的时候加了 -b ,那么就需要查看对应的 tag 所对应的驱动,地址如下:https://developers.google.cn/android/drivers
以我的 pixel 3 XL 为例,下载的 TAG 为 android-12.0.0_r3 (repo init -u git://mirrors.ustc.edu.cn/aosp/platform/manifest -b android-12.0.0_r3)
那么我们需要找到下面的部分,这里的 SP1A.210812.016.A1 跟上面 4.2 节是对应的,即 Tag android-12.0.0_r3 对应的 Build ID 是 SP1A.210812.016.A1。大家可以根据自己下载的 TAG 找到对应的 Build ID,然后根据 Build ID 寻找对应的驱动即可 https://developers.google.cn/android/drivers
跟 4.2 节下载的 Tag 是对应的:
下载的内容解压后,是两个 sh 文件,以我的 Pixel 3 XL 为例,在代码根目录执行,使用 D 来向下翻页,直到最后手动输入 I ACCEPT
1 | # 解压缩 extract-google_devices-crosshatch.sh |
1 | # 解压缩 ./extract-qcom-crosshatch.sh |
代码和驱动都下载好之后,就可以开始代码的编译工作了,由于新版本不再支持 Mac 编译,所以建议大家还是使用 Linux 来进行编译,推荐使用 Ubuntu
参考:https://source.android.google.cn/setup/build/initializing
Ubuntu 18.04 以上直接运行:
1 | sudo apt-get install git-core gnupg flex bison build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig |
每次关闭 Shell 之后都需要重新执行下面这个脚本,相当于配置了一下编译环境
1 | source build/envsetup.sh |
或者
1 | . build/envsetup.sh |
1 | lunch |
运行 lunch 之后,会有一堆设备出来让你选择,还是以我的 Pixel 3 XL 为例,其代号是 ,在这里可以查看所有机型对应的代号:https://source.android.google.cn/setup/build/running#selecting-device-build
Pixel 3 XL 对应的代号是:crosshatch
所以我选择编译的是 aosp_crosshatch-userdebug ,这里可以输入编号也可以直接输入 aosp_crosshatch-userdebug
然后脚本会进行一系列的配置,输出下面的内容
使用 m 构建所有内容。m 可以使用 -jN 参数处理并行任务。如果您没有提供 -j 参数,构建系统会自动选择您认为最适合您系统的并行任务计数。
1 | m |
如上所述,您可以通过在 m 命令行中列出相应名称来构建特定模块,而不是构建完整的设备映像。此外,m 还针对各种特殊目的提供了一些伪目标。以下是一些示例:
运行 m help 即可查看 m 提供的其他命令
输入 m 之后开始第一次全部编译,漫长的等待,编译时间取决于你的电脑配置…主要是 cpu 和内存,建议内存 32G 走起,cpu 也别太烂
编译成功之后,会有下面的输出
自己编译的 UserDebug 固件用来 Debug 是非常方便的,不管是用来 Debug Framework 还是 App
编译好之后下面开始刷机,以我的测试机器 Pixel 3 XL 为例,依次执行下面的命令
1 | adb reboot fastboot |
刷机截图如下
之后手机会自动重启,然后进入主界面,至此,我们的代码下载-编译-刷机的这部分就结束了
自己编译的 AOSP 的 Launcher 比较丑,因为没有 Google 闭源的那些套件的加持,看上去还是很简陋的,自带的 App 非常少,而且基本上没怎么维护,给到手机厂商的就是这么一个东西
还是官方的 Pixel 带的 Launcher 好看(Google 开发和维护)
如果在刷机的过程中遇到问题,可刷官方的刷机包拯救 :https://developers.google.cn/android/images
本文主要是讲如何下载、编译、刷机,后续的代码导入、修改和编译模块、代码 Debug 等,会另起一篇文章来介绍
一个人可以走的更快 , 一群人可以走的更远
如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲
另外在 App 或者厂商内部,卡顿、响应速度、ANR 这几个性能指标都是有单独的标准的,比如 掉帧率、启动速度、ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了
本文是响应速度系列的第三篇,主要是讲在使用 Systrace 分析应用响应速度问题的时候,其中的一些延伸知识,包括启动速度测试、Log 输出解读、Systrace 状态解读、三方启动库等内容
Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了
Systrace 系列文章如下
Systrace 中,进程的任务最常见的有三种状态:Sleep、Running、Runnable。在优化的过程中,这几个状态也需要我们关注。进程任务状态在最上面,以颜色来做区分:
一般白色的 Sleep 有两种,即应用主动 Sleep 和被动 Sleep
如下图,这种在启动过程中,有较长时间的 sleep 情况,一般下面就可以看到是否在进行 Binder 通信,如果在启动过程中有频繁的 Binder 通信,那么应用等待的时间就会变长,导致响应时间变慢
这种一般可以点击这个 Task 最下面的 binder transaction 来查看 Binder 调用信息,比如
有时候没有 Binder 信息,是被其他的等待的线程唤醒,那么可以查看唤醒信息,也可以找到应用是在等待什么
放大上图中我们点击的 Runnable 的地方
Running 状态的任务就是目前在 CPU 某一个核心上运行的任务,如果某一段任务是 Running 状态,且耗时变长,那么需要分析:
在某些 Android 机器上,大家一般会对 App 的主线程和渲染线程进行调度方面的优化:一般前台应用的 UI Thread 和 RenderThread 都是跑在大核上的
一个 Task 要从 Sleep 状态转到 Running 状态,必须先变成 Runnable 状态,其状态转换图如下
在 Systrace 上的表现如下
正常情况下,应用进入 Runnable 状态之后,会马上被调度器调度,进入 Running 状态,开始干活;但是在系统繁忙的时候,应用就会有大量的时间在 Runnable 状态,因为 cpu 已经跑满,各种任务都需要排队等待调度
如果应用启动的时候出现大量的 Runnable 任务,那么需要查看系统的状态
TraceView 指的是我们在 AS Profiler 里面抓取 CPU 信息的时候出现的那个,大家看下面的截图就知道了
使用下面的命令可以抓取应用的冷启动,这些命令也可以分开执行,需要把里面的包名和 Activity 名切换成自己应用的包名
1 | adb shell am start -n com.aboback.wanandroidjetpack/.splash.SplashActivity --start-profiler /data/local/tmp/traceview.trace --sampling 1 && sleep 10 && adb shell am profile stop com.aboback.wanandroidjetpack && adb pull /data/local/tmp/traceview.trace . |
或者分开执行上面的命令
1 | // 1. 冷启动 App,sampleing = 1 意思是 1ms 采样一次 |
抓出来的 TraceView 可以直接在 Android Studio 中打开
其中图里面用绿色标记的函数,就是应用自己的函数,黄色标注的是系统的函数
Application.onCreate
Activity.onCreate
doFrame
WebView 初始化
由于采样比较细,所以会性能损耗比较大,所以抓出来的 TraceView,其中每个方法的执行时间是不准的,所以不可用作为真实的时间参考,但是可以用来定位具体的函数调用栈。
需要跟 Systrace 来进行互补
使用 SimplePerf 工具也可以抓取启动时候的堆栈信息,既包括 Java 也包括 Native
比如我们要抓取 com.aboback.wanandroidjetpack 这个应用的冷启动,可以执行下面的命令(SimplePerf 的环境初始化参考 https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/android_application_profiling.md 这篇文章 ,其中 app_profiler.py 就是 SimplePerf 的工具)
1 | python app_profiler.py -p com.aboback.wanandroidjetpack |
执行上面的命令之后,需要手动在手机上启动 App,然后主动结束
1 | python app_profiler.py -p com.aboback.wanandroidjetpack |
抓取结束之后,调用解析脚本来生成 html 报告
1 | python report_html.py |
就会得到下面这个
不仅可以看到 Java 层的堆栈,也可以看到 Native 的堆栈,这里只是简单的使用,更详细的方法可以参考下面几个文档
SimplePerf 初步试探 https://android.googlesource.com/platform/system/extras/+/master/simpleperf/doc/README.md
1 | public final void scheduleCreateService(IBinder token, |
可以看到,代码执行都是往 H 这个 Handler 中发送 Message,所以如果我们在代码里面启动 Service,并不是马上就执行的,而是由 MessageQueue 里面的 Message 顺序决定的
放大真正执行的部分可以看到,其执行的时机是在 MessageQueue 按照 Message 的顺序执行(这里是在应用第一帧执行结束后),后面的 Message 就是应用自己的 Message、启动 Service、执行广播接收器
执行自定义的 Message 在 Systrace 中的显示
Service 启动在 Systrace 中的显示
执行 Receiver 在 Systrace 中的显示
Broadcast 的注册:一般是在 Activity 生命周期函数中注册,在哪里注册就在哪里执行
很多三方库都需要在 Application 中进行初始化,并顺便获取到 Application 的上下文
但是也有的库不需要我们自己去初始化,它偷偷摸摸就给初始化了,用到的方法就是使用 ContentProvider 进行初始化,定义一个 ContentProvider,然后在 onCreate 拿到上下文,就可以进行三方库自己的初始化工作了。而在 APP 的启动流程中,有一步就是要执行到程序中所有注册过的 ContentProvider 的 onCreate 方法,所以这个库的初始化就默默完成了。
这种做法确实给集成库的开发者们带来了很大的便利,现在很多库都用到了这种方法,比如 Facebook,Firebase,WorkManager
ContentProvider 的初始化时机如下:
但是当大部分三方库使用这种方法初始化的时候,就会有下面几个问题
针对上面的情况,Google 推出了 AppStartup 库,AppStartup 库的优点
根据测算结果来看,使用 AppStartup 库并不能显著加快应用启动速度,除非你有非常多 (50+)的 ContentProvider 在应用启动的时候初始,那么 AppStartup 才会有比较明显的效果
如果三方的 SDK 使用 ContentProvider 初始化耗时,那么可以考虑针对这个 ContentProvider 进行延迟初始化,比如
1 | <provider |
ExampleLoggerInitializer 的 meta-data 当中加入了一个 tools:node=”remove”的标记
在启动优化的过程中,idleHandler 可以在 MessageQueue 空闲的时候执行任务,如下图,可以很清晰地查看 idleHandler 的执行时机
其使用场景如下:
在启动的过程中,可以借助 idleHandler 来做一些延迟加载的事情。比如在启动过程中 Activity 的 onCreate 里面 addIdleHandler,这样在 Message 空闲的时候,可以执行这个任务
进行启动时间统计:比如在页面完全加载之后,调用 activity.reportFullyDrawn 来告知系统这个 Activity 已经完全加载,用户可以使用了,比如下面的例子,在主页的 List 加载完成后,调用 activity.reportFullyDrawn
其对应的 Systrace 如下
这时候得到的应用的冷启动时间才是正常的
另外系统有些功能,也会依赖于 FullyDrawn,所以建议主动上报(即主动在 App 完全启动后调用 activity.reportFullyDrawn)
一个人可以走的更快 , 一群人可以走的更远
]]>如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢 ,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲
另外在 App 或者厂商内部,卡顿、响应速度、ANR 这几个性能指标都是有单独的标准的,比如 掉帧率、启动速度、ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了
本文是响应速度系列的第二篇,主要是以 Android App 冷启动为例,讲解如何使用 Systrace 来分析 App 冷启动
Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了
Systrace 系列文章如下
这个案例和对应的 Systrace 偏工程化一些,省略了很多细节,因为应用的启动流程涉及的知识非常广,如果每个都细化的话,会有很大的篇幅。推荐大家看这篇文章,非常详细:Android 应用启动全流程分析
所以这里以 Systrace 为主线,讲解应用启动的时候各个关键模块的大概工作流程。了解大概流程之后,就可以分段去深入自己感兴趣或者自己负责的部分,这里首先放一张 Systrace 和手机截图所对应的图,大家可以先看看这个图,然后再往下看(博客里面 Perfetto 和 Systrace 混合使用)
为了更方便分析应用冷启动,我们需要做下面的准备工作
adb shell am trace-ipc start
adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt
python /mnt/d/Android/platform-tools/systrace/systrace.py gfx input view webview wm am sm rs bionic power pm ss database network adb idle pdx sched irq freq idle disk workq binder_driver binder_lock -a com.xxx.xxx
,注意这里的 com.xxx.xxx 换成自己的包名,如果不是调试特定的包名,可以去掉 -a com.xxx.xxx本文以 在桌面上冷启动一个 Android App 为例,应用冷启动的整个流程包含了从用户触摸屏幕到应用完全显示的整个流程,其中涉及到
上一篇文章有讲到响应速度问题,需要搞清楚 起点 和 终点,对于应用冷启动来说,起点就是 input 事件,终点就是应用完全展示给用户(用户可操作)
下面将从上面几个关键流程,通过 Systrace 的来介绍整个流程
由于我们的案例是在桌面冷启动一个 App,那么在手指触摸手机屏幕的时候,触摸屏会触发中断,这个中断我们最早能在 Systrace 中看到的地方如下:
对应的 cpu ss 区域和 中断区域(加了 irq 的 tag 才可以看到)
一般来说,点击屏幕会触发若干个中断,这些信号经过处理之后,触摸屏驱动会把这些点更新到 EventHub 中,让 InputReader 和 InputDIspatcher 进行进一步的处理。这一步一般不会出现什么问题,厂商这边对触摸屏的调教可能会关注这里
InputReader 和 InputDispatcher 这两个线程跑在 SystemServer 里面,专门负责处理 Input 事件,具体的流程可以参考Android Systrace 基础知识 - Input 解读 这篇文章
这里由于我们是点击桌面上的一个 App 的图标,可以看到底层上报上来的事件包括一个 Input_Down 事件 + 若干个 Input Move 事件 + 一个 Input Up 事件,组成了一个完整的点击事件
由于 Launcher 在进程创建的时候就注册了 Input 监听,且此时 Launcher 在前台且可见,所以 Launcher 进程可以收到这些 Input 事件,并根据 Input 事件的类型进行处理,input 事件在 SystemServer 和 App 的流转在 Systrace 中的具体表现可以参考 Android Systrace 基础知识 - Input 解读 ,这里把核心的两张图放上来
看下图即可,如果要看更详细的,可以查看 Android Systrace 基础知识 - Input 解读
看下图即可,如果要看更详细的,可以查看 Android Systrace 基础知识 - Input 解读
Launcher 处理 Input 事件也是响应时间的一个重要阶段,主要包括两个响应速度指标
另外提一下,滑动桌面到桌面第一帧响应时间(这个指的是滑动桌面的场景,左右滑动桌面的时候,用高速相机拍摄,从手指动开始,到桌面动的第一帧的时间)也是一个很重要的响应速度指标,部分厂商也会在这方面做优化,感兴趣的可以自己试试主流厂商的桌面滑动场景(跟原生的机器对比 Systrace 即可)
在冷启动的场景里面,Launcher 在收到 up 事件后,会进行逻辑判断,然后启动对应的 App(这里主要是交给 AMS 来处理,又回到了 SystemServer 进程)
这个阶段通常也是做系统优化的会比较关注,做 App 的同学还不需要关注到这里(Launcher App 的除外);另外在最新的版本,应用启动的动画是由 Launcher 和 SystemServer 共同完成的,目的就是可以做一些复杂的动画而没有割裂感,大家可以用慢镜头拍一下启动时候和退出应用的动画,可以看到有的应用图标是分层的,甚至会动,这是之前纯粹由 SystemServer 这边来做动画所办不到的
SystemServer 处理主要是有2部分
这个 SystemServer 进程中的 Binder 调用就是 Launcher 通过 ActivityTaskManager.getService().startActivity 调用过来的
fork 新的进程,则是在判断启动的 Activity 的 App 进程没有启动后,需要首先启动进程,然后再启动 Activity,这里是冷启动和其他启动不一样的地方。fork 主要是 fork Zygote64 这个进程(部分 App 是 fork 的 Zygote32 )
对应的代码如下,这里就正式进入了 App 自己的进程逻辑了
应用启动后,SystemServer 会记录从 startActivity 被调用到应用第一帧显示的时长,在 Systrace 中的显示如下(注意结尾是应用第一帧,如果应用启动的时候是 SplashActivity -> MainActivity,那么这里的结尾只是 SplashActivity,MainActivity 的完全启动需要自己查看)
通常的大型应用,App 冷启动通常包括下面三个部分,每一个部分耗时都会导致应用的整体启动速度变慢,所以在优化启动速度的时候,需要明确知道应用启动结束的点(需要跟测试沟通清楚,一般是界面保持稳定的那个点)
下面针对这三个阶段来具体分析(当然你的 App 如果简单的话,可能没有 SplashActivity ,直接进的就是主 Activity,那么忽略第二步就可以了)
由于是冷启动,所以 App 进程在 Fork 之后,需要首先执行 bindApplication ,这个也是区分冷热启动的一个重要的点。Application 的环境创建好之后,就开始组件的启动(这里是 Activity 组件,通过 Service、Broadcast、ContentProvider 组件启动的进程则会在 bindApplication 之后先启动这些组件)
Activity 的生命周期函数会在 Activity 组件创建的时候执行,包括 onStart、onCreate、onResume 等,然后还要经过一次 Choreographer#doFrame 的执行(包括 measure、layout、draw)以及 RenderThread 的初始化和第一帧任务的绘制,再加上 SurfaceFlinger 一个 Vsync 周期的合成,应用第一帧才会真正显示(也就是下图中 finishDrawing 的位置),这部分详细的流程可以查看 Android Systrace 基础知识 - MainThread 和 RenderThread 解读
大部分的 App 都有 SplashActivity 来播放广告,播放完成之后才是真正的主 Activity 的启动,同样包括 Activity 组件的创建,包括 onStart、onCreate、onResume 、自有启动逻辑的执行、WebView 的初始化等等等等,直到主 Activity 的第一帧显示
一般来说,主 Activity 需要多帧才能显示完全,因为有很多资源(最常见的是图片)是异步加载的,第一帧可能只加载了一个显示框架、而其中的内容在准备好之后才会显示出来。这里也可以看到,通过 Systrace 不是很方便来判断应用冷启动的终点(除非你跟测试约定好,在某个 View 显示之后就算启动完成,然后你在这个 View 里面打个 Systrace 的 Tag,通过跟踪这个 Tag 就可以粗略判断具体 Systrace 里面哪一帧是启动完成的点)
我制作了一个 Systrace + 截图的方式,来进行演示,方便你了解 App 启动各个阶段都对应在 Systrace 的哪里(使用的是一个开源的 WanAndroid 客户端)
本文重点放在了如何在 Systrace 中展示 App 的完整冷启动流程,方便大家在做 App 的启动优化的时候,可以通过 Systrace 来快速定位到启动瓶颈,也方便进行竞品的对比和分析,
一个人可以走的更快 , 一群人可以走的更远
]]>如果是动画播放卡顿、列表滑动卡顿这种,我们一般定义为 狭义的卡顿,对应的英文描述我觉得应该是 Jank;如果是应用启动慢、亮灭屏慢、场景切换慢,我们一般定义为 响应慢 ,对应的英文描述我觉得应该是 Slow ;如果是发生了 ANR,那就是 应用无响应问题 。三种情况所对应的分析方法和解决方法不太一样,所以需要分开来讲
另外在 App 或者厂商内部,卡顿、响应速度、ANR 这几个性能指标都是有单独的标准的,比如 掉帧率、启动速度、ANR 率等,所以针对这些性能问题的分析和优化能力,对开发者来说就非常重要了
本文是响应速度系列的第一篇,主要是讲响应速度相关的理论知识,包括性能工程概述、响应速度涉及到的知识点、响应速度的分析方法和套路等
关于卡顿的文章可以参考这一篇 Systrace 流畅性实战 1 :了解卡顿原理 ,ANR 的文章后续会介绍,本文主要是讲响应速度相关的基本原理
Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了
在介绍响应速度的原理之前,这里先放一段 <**性能之巅**> 这本书中对于性能的描述,具体来说就是方法论,非常贴合本文的主题,也强烈推荐各位搞性能优化的同学,把这本书作为手头常读的方法论书籍:
性能是充满挑战的
系统性能工程是一个充满挑战的领域,具体原因有很多,其中包括以下事实,系统性能是主观的、复杂的,而且常常是多问题并存的
性能是主观的
- 技术学科往往是客观的,太多的业界人士审视问题非黑即白。在进行软件故障查找的时候,判断 bug 是否存在或 bug 是否修复就是这样。bug 的出现总是伴随着错误信息,错误信息通常容易解读,进而你就明白错误为什么会出现了
- 与此不同,性能常常是主观性的。开始着手性能问题的时候,对问题是否存在的判断都有可能是模糊的,在问题被修复的时候也同样,被一个用户认为是“不好”的性能,另一个用户可能认为是“好”的
系统是复杂的
- 除了主观性之外,性能工程作为一门充满了挑战的学科,除了因为系统的复杂性,还因为对于性能,我们常常缺少一个明确的分析起点。有时我们只是从猜测开始,比如,责怪网络,而性能分析必须对这是不是一个正确的方向做出判断
- 性能问题可能出在子系统之间复杂的互联上,即便这些子系统隔离时表现得都很好。也可能由于连锁故障(cascading failure)出现性能问题,这指的是一个出现故障的组件会导致其他组件产生性能问题。要理解这些产生的问题,你必须理清组件之间的关系,还要了解它们是怎样协作的
- 瓶颈往往是复杂的,还会以意想不到的方式互相联系。修复了一个问题可能只是把瓶颈推向了系统里的其他地方,导致系统的整体性能并没有得到期望的提升。
- 除了系统的复杂性之外,生产环境负载的复杂特性也可能会导致性能问题。在实验室环境很难重现这类情况,或者只能间歇式地重现
- 解决复杂的性能问题常常需要全局性的方法。整个系统——包括自身内部和外部的交互——都可能需要被调查研究。这项工作要求有非常广泛的技能,一般不太可能集中在一人身上,这促使性能工程成为一门多变的并且充满智力挑战的工作
可能有多个问题并存
- 找到一个性能问题点往往并不是问题本身,在复杂的软件中通常会有多个问题
- 性能分析的又一个难点:真正的任务不是寻找问题,而是辨别问题或者说是辨别哪些问题是最重要的
- 要做到这一点,性能分析必须量化(quantify)问题的重要程度。某些性能问题可能并不适用于你的工作负载或者只在非常小的程度上适用。理想情况下,你不仅要量化问题,还要估计每个问题修复后能带来的增速。当管理层审查工程或运维资源的开销缘由时,这类信息尤其有用。
- 有一个指标非常适合用来量化性能,那就是 延时(latency)
– 以上几段内容摘录自 <**性能之巅**>
Systrace 系列文章如下
响应速度是应用 App 性能的重要指标之一。响应慢通常表现为点击效果延迟、操作等待或白屏时间长等,主要场景包括:
从原理上来说,响应速度场景往往是由一个 input 事件(以 Message 的形式给到需要处理的应用主线程)触发(比如点击、长按、电源键、指纹等),由一个或者多个 Message 的执行结束为结尾,而这些 Message 中一般都有关键的界面绘制相关的 Message 。衡量一个场景的响应速度,我们通常从事件触发开始计时,到应用处理完成计时结束,这一段时间就称为响应时间。
如下图所示,响应速度的问题,通常就是这些 Message 的某个执行超过预期(主观),导致最终完成的时间长于用户期待的时间
由于响应速度是一个比较主观的性能指标(而流畅度就是一个很精确的指标,掉一帧就是掉一帧),而且根据角色的不同,对这个性能指标的判定也不同,比如 Android 系统开发者和应用开发者以及测试同学,对 应用冷启动 的起点和终点就有不同的判定:
分析响应速度,最重要的是要找到起点和终点,上一节讲到,不同角色的开发者,对这个性能指标的判定起点和终点都不一样;而且这个指标有很主观的成分,所以在开始的时候,就要跟各方来确定好起点和终点,具体的数值标准,下面一些手段可以帮助大家来确定
一般来说,起点都比较好确定,无非是一个点击事件或者一个自定义的触发事件;而终点的确定就比较麻烦,比如如何确定一个复杂的 App (比如淘宝)启动完成的时间点,用 Systrace 的第一帧或者 Log 输出的 Displayed 时间或者 onWindowFocusChange 回调的时间显然是不准确的。目前市面上使用高速相机 + 图像识别来做是一个比较主流的做法
下面这些列举的是 Android 系统自身的原因,与 Android 机器的性能有比较大的关系,性能越差,越容易出现响应速度问题。下面就列出了 Android 系统原因导致的 App 响应速度出现问题的原因,以及这个时候 App 端在 Systrace 中的表现
应用自身原因主要是应用启动时候的组件初始化、View 初始化、数据初始化耗时等,具体包括:
本篇文章主要是一个响应速度基础知识方面的一个普及,其中涉及到大量的系统知识,不熟悉的同学可以跟着 Systrace 基础知识系列 过一下
一个人可以走的更快 , 一群人可以走的更远
]]>Systrace 系列文章如下
Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下上面列出的 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了
这里的 Frame 标记指的是应用主线程上面那个圈,共有三个颜色,每一帧的耗时不同,则标识的颜色不同
点击这个小圆圈就可以看到这一帧所对应的主线程+渲染线程(会以高亮显示,其他的则变灰显示)
绿帧是最常见的帧,表示这一帧在一个 Vsync 周期里面完成
黄帧表示这一帧耗时超过1个 Vsync 周期,但是小于 2 个 Vsync 周期。黄帧的出现表示这一帧可能存在性能问题,可能会导致卡顿情况出现
红帧表示这一帧耗时超过 2 个 Vsync 周期,红帧的出现表示这一帧可能存在性能问题,大概率会导致卡顿情况出现
不一定,判断是否掉帧要看 SurfaceFlinger,而不是看 App ,这部分需要有 https://www.androidperformance.com/2019/12/15/Android-Systrace-Triple-Buffer/ 这篇文章的基础
如上所述,红帧和黄帧都表示这一帧存在性能问题,黄帧表示这一帧耗时超过一个 Vsync 周期,但是由于 Android Triple Buffer(现在的高帧率手机会配置更多的 Buffer)的存在,就算 App 主线程这一帧超过一个 Vsync 周期,也会由于多 Buffer 的缓冲,使得这一帧并不会出现掉帧
这次分析的 Systrace(见附件),就是没有红帧只有黄帧,连续出现两个黄帧,第一个黄帧导致了卡顿,而第二个黄帧则没有
还是这个 Systrace(附件) 中的情况,第一个疑点处两个黄帧,可以看到第二个黄帧的主线程耗时很久,这时候不能单纯以为是主线程的问题(因为是 Sleep 状态)
如下图所示,是因为前一帧的渲染线程超时,导致这一帧的渲染线程任务在排队等待,如(https://www.androidperformance.com/2019/11/06/Android-Systrace-MainThread-And-RenderThread/)这篇文章里面的流程,主线程是需要等待渲染线程执行完 syncFrameState 之后 unblockMainThread,然后才能继续。
还是这个场景(桌面左右滑动),卡顿是发生在松手之后的,如果一直不松手,那么就不会出现卡顿,这是为什么?
如下图,可以看到,如果不松手,cpu 这里会有一个持续的 Boost,且此时 RenderThread 的任务都跑在 4-6 这三个大核上面,没有跑到小核,自然也不会出现卡顿情况
这一段 Boost 的 Timeout 是 120 ms,具体的配置每个机型都不一样,熟悉 PerfLock 的应该知道,这里就不多说了
如果这个场景不卡,那么我们怎么衡量两台不同的机器在这个场景下的性能呢?
可以使用 adb shell dumpsys gfxinfo
,使用方法如下
adb shell dumpsys gfxinfo com.miui.home framestats reset
,这一步的目的是清除历数据adb shell dumpsys gfxinfo com.miui.home framestats
这时候会有一堆数据输出,我们只需要关注其中的一部分数据即可我们拿这个场景,跟 Oppo Reno 5 来做对比,只取我们关注的一部分数据
下面是一些对比,可以看到小米在桌面滑动这个场景,性能是要弱于 Oppo 的
另外 GPU 的数据也比较有趣,小米的高通 865 配的 GPU 要比 Reno 5 Pro 配的 GPU 要强很多,所以 GPU 的数据小米要比 Reno 5 Pro 要好,也可以推断出这个场景的瓶颈在 CPU 而不是在 GPU
可能有下面几种情况
附件已经上传到了 Github 上,可以自行下载:https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action
一个人可以走的更快 , 一群人可以走的更远
]]>Systrace 系列文章如下
Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下上面列出的 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了
使用 Systrace 分析卡顿问题,我们一般的流程如下
复现卡顿的场景,抓取 Systrace,可以用 shell 或者手机自带的工具来抓取
双击抓出来的 trace.html 直接在 Chrome 中打开 Systrace 文件
分析卡顿问前,我们需要了解问题发生的背景,以提高分析 Systrace 的效率
分析问题之前或者分析的过程中,也可以通过检查 Systrace 来了解一些基本的信息
定位 App 进程在 Systrace 中的位置
打开 Systrace 后,首先要首先要看的就是 App 进程,主要是 App 的主线程和渲染线程,找到 Systrace 中每一帧耗时的部分,比如下面这种,可以看到 App 的 UI Thread 的红框部分,耗时 110ms,明显是不正常的(这个案例是 Bilibili 列表滑动卡顿)
事实上,所有超过一个 Vsync 周期的 doFrame 耗时(即大家看到的黄帧和红帧),我们都需要去看一下是否真的发生的掉帧,就算没有掉帧,也要看一下原因,比如下面这个
Vsync 周期与刷新率的对照
分析 SurfaceFlinger 进程的主线程和 Binder 线程
由于多个 Buffer 缓冲机制存在,App 主线程和渲染线程,有时候即使超过一个 Vsync 周期,也不一定会出现卡顿,所以这里我们需要看SurfaceFlinger 进程的主线程,来确认是否真的发生了卡顿 ,比如上面 3.1 部分的图,App 主线程耗时 110 ms,对应的 SurfaceFlinger 主线程如下
Systrace 中的 SurfaceFlinger 进程区域,对应的 App 的 Buffer 个数也是空的
从整机角度分析和 Binder 调用分析(不涉及可以不用看)
按照这个流程分析之后,需要再反过来看各个进程,把各个线索联系起来,推断最有可能的原因
App 掉帧的原因非常多,有 APP 本身的问题,有系统原因导致卡顿的,也有硬件层的、整机卡的,这个可以参考下面四篇文章,不同的卡顿原因在 Systrace 中会有不同的表现
Systrace 作为分析卡顿问题的第一手工具,给开发者提供了一个从手机全局角度去看问题的方式,通过 Systrace 工具进行分析,我们可以大致确定卡顿问题的原因:是系统导致的还是应用自身的问题
当然 Systrace 作为一个工具,再进行深入的分析的时候就会有点力不从心,需要配合 TraceView + 源码来进一步定位和解决问题,最后再使用 Systrace 进行验证
所以本文更多地是讲如何发现和分析卡顿问题,至于如何解决,就需要后续自己寻找合适的解决方案了,比如对比竞品的 Systrace 表现、优化代码逻辑、优化系统调度、优化布局等
个人在使用小米 10 Pro 的时候,在桌面滑动这个最常用的场景里面,总会有一种卡顿的感觉,10 Pro 是 90Hz 的屏幕,FPS 也是 90,所以一旦出现卡顿,就会有很明显的感觉(个人对这个也比较敏感)。之前没怎么关注,在升级 12.5 之后,这个问题还是没有解决,所以我想看看到底是怎么回事
抓了 Systrace 之后分析发现,这个卡顿场景是一个非常好的案例,所以把这个例子拿出来作为流畅度的一个实战分享
建议大家下载附件中的 Systrace,对照文章食用最佳
- 鉴于卡顿问题的影响因素比较多,所以在开始之前,我把本次分析所涉及的硬件、软件版本沟通清楚,如果后续此场景有优化,此文章也不会进行修改,以文章附件中的 Systrace 为准
- 硬件:小米 10 Pro
- 软件:MIUI 12.5.3 稳定版
- 小米桌面版本:RELEASE-4.21.11.2922-03151646
这次抓的 Systrace 我只滑动了一次,所以比较好定位,滑动的 input 事件由一个 Input Down 事件 + 若干个 Input Move 事件 + 一个 Input Up 事件组成
在 Systrace 中,SystemServer 中的 InputDispatcher 和 InputReader 线程都有体现,我们这里主要看在 App 主线程中的体现
如上图,App 主线程上的 deliverInputEvent 标识了应用处理 input 事件的过程,input up 之后,就进入了 Fling 阶段,这部分的基础知识可以查看下面这两篇文章
由于这次卡顿主要是松手之后才出现的,所以我们主要看 Input Up 之后的这段
主线程上面的 Frame 有颜色进行标注,一般有绿、黄、红三种颜色,上面的 Systrace 里面,没有红色的帧,只有绿色和黄色。那么黄色就一定是卡顿么?红色就一定是卡顿么?其实不一定,单单通过主线程,我们并不能确定是否卡顿,这个在下面会讲
从主线程我们没法确定是否发生了卡顿,我们找出了三个可疑的点,接下来我们看一下 RenderThread
放大第一个可疑点,可以看到,这一帧总耗时在 19ms, RenderThread 耗时 16ms,且 RenderThread 的 cpu 状态都是 running(绿色),那么这一帧这么耗时的原因大概率是下面两个原因导致的:
由于只是可疑点,所以我们先不去看 cpu 相关的,先查看 SurfaceFlinger 进程,确定这里有卡顿发生
对于 Systrace 中 SurfaceFlinger 部分解读不熟悉的可以先预习一下这篇文章 https://www.androidperformance.com/2020/02/14/Android-Systrace-SurfaceFlinger/
这里我们主要看两个点
判断是否卡顿的标准如下
如果 SurfaceFlinger 主线程没有合成任务,而且 App 在这一个 Vsync 周期(vsync-app)进行了正常的工作,但是对应的 App 的 BufferQueue 里面没有可用的 Buffer,那么说明这一帧卡了 — 卡顿出现
这种情况如下图所示(也是上图中第一个疑点所在的位置)
如果 SurfaceFlinger 进行了合成,而且 App 在这一个 Vsync 周期(vsync-app)进行了正常的工作,但是对应的 App 的 BufferQueue 里面没有可用的 Buffer,那么这一帧也是卡了,之所以 SurfaceFlinger 会正常合成,是因为有其他的 App 提供了可用来合成的 Buffer — 卡顿出现
这种情况如下图所示(也在附件的 Systrace 里面)
如果 SurfaceFlinger 进行了合成,而且 App 在这一个 Vsync 周期(vsync-app)进行了正常的工作,而且对应的 App 的 BufferQueue 里面有可用的 Buffer,那么这一帧就会正常合成,此时没有卡顿出现 — 正常情况
正常情况如下,作为对比还是贴上来方便大家对比
回到本例的第一个疑点的地方,我们通过 SurfaceFlinger 端的分析,发现这一帧确实是掉了,原因是 App 没有准备好可用的 Buffer 供 SurfaceFlinger 来合成,那么接下来就需要看为什么这一帧 App 没有可用的 Buffer 给到 SurfaceFlinger
上面我们分析这一帧所对应的 MainThread + RenderThread 耗时在 19ms,且 RenderThread 耗时就在 16ms,那么我们来看 RenderThread 的情况
出现这种情况主要是有下面两个原因
但是桌面滑动这个场景,负载并不高,且松手之后并没有多余的操作,View 更新之类的,本身耗时比前一帧多了将近 3 倍,可以推断不是自身负载加重导致的耗时
那么就需要看此时的 RenderThread 的 cpu 情况:
既然在 Running 情况,我们就去 CPU Info 区域查看这一段时间这个任务的调度情况
查看 CPU (Kernel 区域,这部分的基础知识可以查看 Android Systrace 基础知识 - CPU Info 解读 和 Android Systrace 基础知识 – 分析 Systrace 预备知识)这两篇文章
回到这个案例,我们可以看到 App 对应的 RenderThread 大部分跑在 cpu 2 和 cpu 0 上,也就是小核上(这个机型是高通骁龙 865,有四个小核+3 个大核+1 个超大核)
其此时对应的频率也已经达到了小核的最高频率(1.8Ghz)
且此时没有 cpu boost 介入
那么这里我们猜想,之所以这一帧 RenderThread 如此耗时,是因为小核就算跑满了,也没法在这么短的时间内完成任务
那么接下来要验证我们的猜想,需要进行下面两个步骤
在用同样的流程分析了后面几个掉帧之后,我们发现
至此,这一次的卡顿分析我们就找到了原因:RenderThread 掉到了小核
至于 RenderThread 的任务为啥跑着跑着就掉到了小核,这个跟调度器是有关系的,大小核直接的调度跟任务的负载有关系,任务从大核掉到小核、或者从小核迁移到大核,调度器这边都是有参数和算法来控制的,所以后续的优化可能需要从这方面去入手
在 Triple-Buffer-的作用 这篇文章,讲到了 Triple Buffer 几个作用
那么在桌面滑动卡顿这个案例里面,Triple Buffer 发挥了什么作用呢?结论是:有的场景没有发挥作用,反而有副作用,导致卡顿现象更明显,下面是分析流程
可以看文章中 Triple Buffer 缓解掉帧 的原理:
在分析小米桌面滑动卡顿这个案例的时候,我发现在有一个问题,小米桌面对应的 App 的 BufferQueue,有时候会出现可用 Buffer 从 2 →0 ,这相当于直接把一个 Buffer 给抛弃掉了,如下图所示
这样的话,如果在后续的桌面 Fling 过程中,又出现了一次 RenderThread 耗时,那么就会以卡顿的形式直接体现出来,这样也就失去了 Triple Buffer 的缓解掉帧的作用了
下图可以看到,由于丢弃了一个 Buffer,导致再一次出现 RenderThread 耗时的时候,表现依然是无 Buffer 可用,出现掉帧
仔细看前面这段丢弃 Buffer 的逻辑,也很容易想到,这里本身就已经丢了一帧了,还把这个耗时帧所对应的 Buffer 给丢弃了(也可能丢弃的是第二帧),不管是哪种情况,滑动时候的每一帧的内容都是计算好的(参考 List Fling 的计算过程),如果把其中一帧丢了,再加上本身 SurfaceFlinger 卡的那一下,卡顿感会非常明显
举个例子,以滑动为例,offset 指的是离屏幕一个左边的距离
2→4→6→8→10→12
2→4→6→6→8→10→12
(假设 计算 8 的这一帧超时了,就会看到两个 6 ,这是掉了一帧的情况)2→4→6→6→10→12
,直接从 6 跳到了 10,相当于卡了 1 次,步子扯大了一次,感官上会觉得卡+跳跃附件已经上传到了 Github 上,可以自行下载:https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action
一个人可以走的更快 , 一群人可以走的更远
]]>Systrace 系列文章如下
Systrace (Perfetto) 工具的基本使用如果还不是很熟悉,那么需要优先去补一下上面列出的 Systrace 基础知识系列,本文假设你已经熟悉 Systrace(Perfetto)的使用了
如文章开头所述,本文主要是分析流畅度相关的问题。流畅度是一个定义,我们评价一个场景的流畅度的时候,往往会使用 fps 来表示。比如 60 fps,意思是每秒画面更新 60 次;120 fps,意思是每秒画面更新 120 次。如果 120 fps 的情况下,每秒画面只更新了 110 次(连续动画的过程),这种情况我们就称之为掉帧,其表现就是卡顿,fps 对应的也从 120 降低到了 110 ,这些都可以被精确地监控到
同时掉帧帧的原因非常多,有 APP 本身的问题,有系统原因导致卡顿的,也有硬件层的、整机卡的,这个可以参考下面四篇文章
用户在使用手机的过程中,卡顿是最容易被感受到的
所以不管是应用还是系统,都应该尽量避免出现卡顿,发现的卡顿问题最好优先进行解决
为了知道卡顿是如何发生的,我们需要知道应用主线程的一帧是如何工作的
从 Choreographer 收到 Vsync 开始,到 SurfaceFlinger/HWC 合成一帧结束(后面还包含屏幕显示部分,不过是硬件相关,这里就不列出来了)
上面的流程图从 Systrace (Perfetto)的角度来看会更加直观
具体的流程参考上面两个图以及代码就会很清楚了,上述整体流程中,任何一个步骤超时都有可能导致卡顿,所以分析卡顿问题,需要从多个层面来进行分析,比如应用主线程、渲染线程、SystemServer 进程、SurfaceFlinger 进程、Linux 区域等
我对卡顿的定义是:稳定帧率输出的画面出现一帧或者多帧没有绘制 。对应的应用单词是 Smooth VS Jank
比如下图中,App 主线程有在正常绘制的时候(通常是做动画或者列表滑动),有一帧没有绘制,那么我们认为这一帧有可能会导致卡顿(这里说的是有可能,由于 Triple Buffer 的存在,这里也有可能不掉帧)
下面从三个方面定义卡顿
这里没有提到应用主线程,是因为主线程耗时长一般会间接导致渲染线程出现延迟,加大渲染线程执行超时的风险,从而引起卡顿;而且应用导致的卡顿原因里面,大部分都是主线程耗时过长导致的
卡顿还要区分是不是逻辑卡顿,逻辑卡顿指的是一帧的渲染流程都是没有问题的,也有对应的 Buffer 给到 SurfaceFlinger 去合成,但是这个 App Buffer 的内容和上一帧 App Buffer 相同(或者基本相同,肉眼无法分辨),那么用户看来就是连续两帧显示了相同的内容。这里一般来说我们也认为是发生了卡顿(不过还要区分具体的情况);逻辑卡顿主要是应用自身的代码逻辑造成的
由于卡顿的原因比较多,如果要分析卡顿问题,首先得对 Android 系统运行的机制有一定的了解,下面简单介绍一下分析卡顿问题需要了解的系统运行机制:
App 进程在创建的时候,Fork 完成后会调用 ActivityThread 的 main 方法,进行主线程的初始化工作
1 | frameworks/base/core/java/android/app/ActivityThread.java |
主线程初始化完成后,主线程就有了完整的 Looper、MessageQueue、Handler,此时 ActivityThread 的 Handler 就可以开始处理 Message,包括 Application、Activity、ContentProvider、Service、Broadcast 等组件的生命周期函数,都会以 Message 的形式,在主线程按照顺序处理,这就是 App 主线程的初始化和运行原理,部分处理的 Message 如下
1 | frameworks/base/core/java/android/app/ActivityThread.java |
这部分可以看 Android Systrace 基础知识 - MainThread 和 RenderThread 解读 这篇文章
上一节应用的主线程初始化完成后,主线程就进入阻塞状态,等待 Message,一旦有 Message 发过来,主线程就会被唤醒,处理 Message,处理完成之后,如果没有其他的 Message 需要处理,那么主线程就会进入休眠阻塞状态继续等待
从下图可以看到 ,Android Message 机制的核心就是四个:Handler、Looper、MessageQueue、Message
网上有很多关于 Message 机制代码细节的分析,所以这里只是简单介绍 Message 机制的四个核心组件的作用
从第一节 App 主线程运行原理可知,ActivityThread 的就是利用 Message 机制,处理 App 各个生命周期和组件各个生命周期的函数
首先我们需要知道什么是屏幕刷新率,简单来说,屏幕刷新率是一个硬件的概念,是说屏幕这个硬件刷新画面的频率:举例来说,60Hz 刷新率意思是:这个屏幕在 1 秒内,会刷新显示内容 60 次;那么对应的,90Hz 是说在 1 秒内刷新显示内容 90 次
与屏幕刷新率对应的,FPS 是一个软件的概念,与屏幕刷新率这个硬件概念要区分开,FPS 是由软件系统决定的 :FPS 是 Frame Per Second 的缩写,意思是每秒产生画面的个数。举例来说,60FPS 指的是每秒产生 60 个画面;90FPS 指的是每秒产生 90 个画面
VSync 是垂直同期( Vertical Synchronization )的简称。基本的思路是将你的 FPS 和显示器的刷新率同期起来。其目的是避免一种称之为”撕裂”的现象.
一般来说,屏幕刷新率是由屏幕控制的,FPS 则是由 Vsync 来控制的,在实际的使用场景里面,屏幕刷新率和 FPS 一般都是一一对应的,具体可以参考下面两篇文章:
上一节讲到 Vsync 控制 FPS,其实 Vsync 是通过 Choreographer 来控制应用刷新的频率的
Choreographer 的引入,主要是配合 Vsync,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 至于为什么 Vsync 周期选择是 16.6ms (60 fps) ,是因为目前大部分手机的屏幕都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每隔 16.6 ms,Vsync 信号到来唤醒 Choreographer 来做 App 的绘制操作 ,如果每个 Vsync 周期应用都能渲染完成,那么应用的 fps 就是 60,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用
Choreographer 扮演 Android 渲染链路中承上启下的角色
下图就是 Vsync 信号到来的时候,Choreographer 借助 Message 机制开始一帧的绘制工作流程图
这部分详细的流程可以看 Android 基于 Choreographer 的渲染机制详解 这篇文章
BufferQueue 是一个生产者(Producer)-消费者(Consumer)模型中的数据结构,一般来说,消费者(Consumer) 创建 BufferQueue,而生产者(Producer) 一般不和 BufferQueue 在同一个进程里面
在 Android App 的渲染流程里面,App 就是个生产者(Producer) ,而 SurfaceFlinger 是一个消费者(Consumer),所以上面的流程就可以翻译为
知道了 Buffer 流转的过程,下面需要说明的是,在目前的大部分系统上,每个应用都有三个 Buffer 轮转使用,来减少由于 Buffer 在某个流程耗时过长导致应用无 Buffer 可用而出现卡顿情况
下图是双 Buffer 和 三 Buffer 的一个对比图
三 Buffer 的好处如下
坏处就是 Buffer 多了会占用内存
这部分详细的流程可以看 Android Systrace 基础知识 - Triple Buffer 解读 这篇文章
Android 系统是由事件驱动的,而 input 是最常见的事件之一,用户的点击、滑动、长按等操作,都属于 input 事件驱动,其中的核心就是 InputReader 和 InputDispatcher。InputReader 和 InputDispatcher 是跑在 SystemServer 里面的两个 Native 线程,负责读取和分发 Input 事件,我们分析 Systrace 的 Input 事件流,首先是找到这里。
上面流程对应的 Systrace 如下
这部分详细的流程可以看 Android Systrace 基础知识 - Input 解读 这篇文章
附件已经上传到了 Github 上,可以自行下载:https://github.com/Gracker/SystraceForBlog/tree/master/Android_Systrace_Smooth_In_Action
一个人可以走的更快 , 一群人可以走的更远
]]>但是这个视频描述和底下的猜测都不对,我这边总结一下这个现象: 微博这个 App 在华为的手机上,在主页列表上下滑动的情况下依然可以流畅加载图片,而同一个版本的微博客户端,安装到其他手机上,在主页列表上下滑动的情况下,则必须要等到滑动停止之后才会加载图片
下面就针对这个现象,从技术的角度来深入分析产生这种现象的原因,以及我们能从里面学到什么
这个现象有什么特别呢 ?
上面三个是从现象上来说的,下面就从技术上来验证,从最后的结果来看,华为和微博的合作毫无疑问是很成功的,可以作为一个案例推广到其他头部 App,同时作为 Android 开发者,对华为这种非常细致的体验优化真的是非常敬佩
“微博这个 App 在华为的手机上,在主页列表上下滑动的情况下依然可以流畅加载图片” 这个现象是因为华为和微博做了联合优化,主要是为了优化微博列表滑动时候的用户体验,其优化点如下
对细节感兴趣的同学可以继续阅读,有能力的同学看完后可以修改 Framework 相关代码,编译一个 SDK,然后自己写个 Demo 接入 SDK,就可以打通我下面所说的所有内容了,我自己在 AOSP 的代码上实现了一遍,Demo 也可以正常运行,有兴趣可以跟我私下交流
我们在滑动微博列表的时候,一个滑动操作主要由下面三部分组成
而华为和微博的优化主要在阶段一和阶段二
技术分析的代码主要来源于微博 apk 的反编译,微博版本 10.8.1,通过反编译的代码可以看到, 微博主页在初始化的时候,会接入华为提供的 PerfSDK,从而获得监听列表滑动速度的能力
列表的 ScrollStateChange 是标识列表状态的一个回调,微博在 ScrollStateChange 这个回调中会根据当前的状态来决定是否加载图片, 从下面的代码逻辑来看
微博的主页在初始化的时候,会给首页的 ListView 注册一个 HwPerfVelocityCallback,从名字可以看出来,这个回调是监听 Velocity 的,也就是滑动的速度,两个回调:
下图为反编译后的源码
至于滑动曲线,则需要查看华为的 Framework 的代码,由于代码量比较大,这里只贴一下 OverScroller.java 中的 update 方法,具体感兴趣的可以自己去翻一番华为的 Framework 代码
计算 Distance 的代码
计算 Velocity 的代码
关于滑动曲线的解释,大家可以看这一篇知乎回答,其中对比了 iOS 和 Android 的滑动曲线的不同 :为什么 iOS 的过渡动画看起来很舒服?
上面图中代码最后一段还有一个判断开关, 如果 boolean a = HwPerfUtil.m14290a 这个返回的是 false,这就是说有可能华为这个优化关闭了,有可能是非华为机器,那么会 判断 Android 版本号和全局 Feature 开关
对应的 FeedAbManager 就是一个 Feature 管理器,可以在线开关某些 Feature
而 m52580k 的实现如下
可以看到这里还受到一个全局的 Feature 配置:feed_scroll_loadimage_enable,这个 Feature 是服务端可以配置的
这里就是处理其他厂商的逻辑
滑动点击是个什么问题呢?列表在滑动的过程中,如果用户点击列表的某一个 Item,那么根据 Android 的事件分发机制,这时候列表的 Item 并不会被点击到,而是整个列表先收到点击事件,然后 触发列表滑动停止;列表停止的时候点击 Item 才会触发 Item 的点击
上面阶段二的优化中,在优化了滑动曲线之后,列表处于 Fling 状态的时间变长,如果用户想点击去某一个 Item 查看详情,那么需要先点击一下让列表停止,然后再点击一下才能进去,这就是这一节想说的 :滑动点击问题
滑动(Fling 状态)和点击其实是需要一个平衡的,这个平衡需要开发者自己去把控:
滑动(Fling 状态)的时间越短,列表越容易停下,用户点击列表越容易触发 Item 的点击,但是容易停止带来的问题就是不够柔和。想象你在粗糙的水泥地上滑出去一块石头,这石头没有滑动多久就会停止,不管是扔石头的你还是旁边看你扔石头的我,都不会觉得这有什么美感,但是没得选。这个的 代表其实就是 Android 原生的 Fling 曲线
滑动(Fling 状态)的时间越长,滑动(Fling 状态)的时间越长,列表越不容易停下,用户点击列表越不容易触发 Item 的点击,如果曲线优化的好,给人的感觉就是很柔和,符合物理规律,想象你在光滑的冰面上滑出去一块冰,冰面越滑,冰块滑动的时间就越长,越不容易停下。这其中的极端代表就是 iOS 的 Fling 曲线。说 iOS 极端是因为,iOS 的滑动曲线调的太磨叽了,时间长不说,停的异常慢,很多时候你都需要点击一下列表让他先停止,然后再进行下一步的点击动作。而小米的 MIUI12 对这个也进行了调整,效果要比 iOS 好一些,如果再和三方进行类似华为和微博的合作,体验会更上一层楼
滑动点击问题其实也可以通过厂商和 App 合作来解决,比如,当滑动到整个滑动距离的 98%(或者 95%) 之后,用户点击列表不再是让列表停止,而是列表内的 item 响应这个点击。这个思想来源于 Launcher 的代码,Launcher 的每一页在左右滑动的时候,如果滑动还没有停止但是用户比较手速快点击了某个 icon 想启动,那么这时候不会触发 Page 停止,而是直接响应 icon 的点击启动应用操作
前文有提到这个问题,滑动的时候进行图片加载主要有两个问题:
滑动中加载图片最大的风险其实就是造成卡顿,因为图片加载本身就是一个比较重的操作,而高帧率的手机上,一帧的时间被压缩到很短,任何小的不确定性都有可能造成卡顿
所以厂商+应用的这个优化: 快速滑动不加载图片,慢速的时候再加载,然后优化滑动曲线 ,其实对厂商和应用都是非常有益处的
下面的 AbsListView 的 OnScrollListener 里面标注了列表滑动的三个状态
两个回调
1 | public interface OnScrollListener { |
TOUCH_SCROLL、FLING、IDLE 三个状态对应的列表滑动操作如下
这三个状态的变化情况如下
Fling 触发之后,每一帧都会调用 update 函数来更新 distance 和 mCurrVelocity,所以我们只需要监听 mCurrVelocity 的值,超过一定的阈值,就可以回调给 App
frameworks/base/core/java/android/widget/OverScroller.java
1 | boolean update() { |
微博这个优化就是厂商和应用之间联合优化的一个案例,应用对用户体验的极致追求,让这种合作在未来会变得更加频繁,像微信、快手、抖音这些…
下面这个招聘是拼多多的一个 JD,看职位描述是专门对接厂商的优化,也可以看出应用对厂商的合作越来越重视。之前厂商和应用是魔高一尺道高一丈的关系,互相攻防导致最终体验受损的还是用户;而现在这种厂商和应用合作的关系,不仅提升了双方的体验,也会带动 Android 生态圈向好的方面去发展
微信公众号 - https://mp.weixin.qq.com/s/wJKOvU7CqP3vM0TG7rO66g
知乎专栏(求个赞) - https://zhuanlan.zhihu.com/p/191460094
一个人可以走的更快 , 一群人可以走的更远
]]>知乎的部分回答中, 大家更是对三星的家属送上了亲切的问候, 甚至有的人已经将这次事故与 Note7 事件、充电门、绿屏门事件相提并论, 甚至预言三星因此会退出国内市场 ; 有的人因为这个丢了 Offer , 有的人准备了很久的资源丢失, 有的人甚至直接把手机砸了…
作为一个 Android 开发者, 我并不想对三星落井下石 , 我只想搞清楚到底是什么原因导致了这场事故 , 以及我们能从里面学到什么 . 我认为既然是 Android 系统出了问题, 我们有必要从技术的角度来分析为什么会出现这样的问题
甚至商场里的机器都变砖了
结论先行, 对于不喜欢看长文的吃瓜群众来说, 直接看结论即可:
这次事故表现是一部分三星用户的手机系统中关键系统服务重复 Crash 并强制进入 Recovery 界面. 关键系统服务指的是三星的 SystemUI 进程 , SystemUI 进程在初始化 AOD 插件的时候 AOD 插件初始化出错导致 SystemUI Crash, 由于是系统服务, SystemUI Crash 到一定的次数之后, 就会强制进入 Recovery 界面, 所以 大部分用户看到的都是 Recovery 界面(下面有图)
AOD 全称 Always On Display, 中文翻译是息屏显示, 就是你按电源键锁屏后, 在屏幕上显示时间、天气、图案等的服务, 这个只有部分高端机型才有这个功能.
AOD Crash 的原因是 2020 年 5 月 23 是闰四月, AOD 显示阴历的时候, 需要显示闰四月, 所以在代码中会走到显示闰四月这个一般很难走进的分支条件, 走进这个条件之后, 需要获取 common_data_leap_month 这个字段, 但是由于代码编译出现了 Bug, 导致无法找到这个字段, 所以该进程直接报了 FATAL EXCEPETION, 进程重启, 重启之后还是要获取这个字段, 再重启, 如此反复 , 最终触发系统的自救措施, 进入 Recovery 界面
这也是为什么只有中国用户才会出现这个问题, 就是因为 AOD 在 5 月 23 号需要显示”闰”四月 , 但是没找到 “闰” 这个字, 所以就挂了 . 所以并不是千年虫 , 也不是服务器被黑, 更不是三星故意恶心人, 这种编译导致的 Bug , 再碰上几年一遇的闰月 , 遇到了就认了吧 , 老老实实道歉, 不丢人.
吃瓜群众可以折返了, 感兴趣的 Android 开发者可以继续往下看, 内容虽然简单, 但是个人觉得还是可以看一下的
对于三星的开发人员来说, 分析这个 Crash 非常简单, 直接在监控里面捞 Log 就可以了, 从后面的分析来看, 这个问题也很快被发现, 并进行了修复(持续了半年左右);但是对这个问题感兴趣的其他开发者来说, 需要借助其他的工具
不过分析的过程也非常简单, 这里会把自己分析的思路和用到的工具记录下来, 方便大家使用
上面结论有说, Persist 进程频繁 Crash 会导致系统触发自救, 进入 Recovery 界面, 所以用户很多反馈截图大家看到都是 Recovery 界面 , 如下
不过也有用户的界面直接显示了报错信息(我猜测是三星这边自己加的功能吧, 知道的麻烦告知一下), 这个界面对我们分析代码来说很重要
开发者对于这个堆栈是最熟悉不过了, 这是在一帧的渲染流程中, AOD 的 LocalDataView 在初始化的时候, 调用 getLunarCalendarInChina 方法出错了, LunarCalendar 是阴历的意思, 报错主要是因为找不到 common_data_leap_month 这个 string 值.
那么问题就很清楚了, 我们只需要查下面两个点
首先看 common_data_leap_month 字段出现的代码逻辑, 既然上面已经列出了函数堆栈, 那么我们需要直接查看代码来分析这个问题产生的逻辑, 如何拿到代码?自然是需要反编译, 推荐的反编译工具: TTDeDroid
反编译需要三星 AOD 的代码, 可以在 ApkMirror 里面搜 Always-On-Display, 就可以找到对应的文件, 可以看到三星的 AOD 更新的频率还是很频繁的, 通过用户反馈可以知道, 并非所有的用户都有这个问题, 且更新到新版本就没有问题了, 那么我们推测问题是出在老版本上的( 从堆栈来猜测应该是 V4.0 的版本 )
最新版本是正常的, 没有 Crash 的情况
首先我们先看一下最新版本这一段代码的逻辑
1 | String month = shouldShowLeapMonth(locale) ? context.getResources().getString(R.string.common_date_leap_month) + months[convertMonth] : months[convertMonth] |
这个就是说如果需要显示闰月, 那就取 common_date_leap_month 的值, 全局搜索 common_date_leap_month 发现最新版本里面是有定义这个值的 .
这里就可以看到 common_data_leap_month 字段出现的代码逻辑 : 只有需要显示闰月的时候, 才会去获取 common_date_leap_month 这个字段的值, 其他 99.9% 的时候都不会触发这个值的获取 .
R.java 文件里面存在的 common_date_leap_month, 说明是存在的, 查看对应的 string.xml 中也有这个字段的定义
既然新版本没有问题, 且我们也知道了 common_date_leap_month 这个字段的代码逻辑 , 那么我们从老版本来看 common_date_leap_month 这个字段没有找到的原因.
这里找的这个老版本是有问题的, 使用这个版本(这几个版本) 的用户到了 23 号会出现频繁 Crash 的现象. 之所以我认为他是有问题的 , 是因为全局搜索 common_date_leap_month 字段, 发现 R 文件里面没有对应的字段, 对应的 string.xml 里面也没有这个字段和他对应的值, 也就是说 , 这里代码只使用, 没有定义和赋值( 那怎么编译过的呢 ???)
上面对应的代码逻辑如下, 可以看到函数名和行数和报错是一致的, InChina….
具体对应的代码:
1 | private String getLunarCalendarInChina(Context context) { |
根据我这边的调查, 发现这个问题其实在 AOD 这个应用从 v3.3.18 升级到 v4.0.57 的时候就出现了, 但是中间一直都没有出问题, 没有闰四月, 用户也就不会有问题, 测试也没有测出来, 直到 2019 年 6 月 24 号发布的 v4.2.44 版本才修复了这个问题
v3.3.18 版本我们可以看到, common_date_leap_month 这个字段还是存在的
升级到 v4.0.57(第一个出问题的版本) 之后 , 这个字段就没有了( 那怎么编译过的呢 ???)
理一下:
这期间所有 AOD 版本在 v4.0.57 - v4.2.44 却从来没有升级的机型, 都会在 2020-5-23 号这一天进入 Recovery 模式.
上面一个很重要的点就是编译问题, Android 开发者都知道, 如果我在代码中写 getString(R.string.common_date_leap_month) , 那我得在 strings.xml 里面定义这个 common_date_leap_month, 然后给他赋值, 比如 “闰” , 这样才能在 R 文件中看到这个字段, 我们才能使用 getString(R.string.common_date_leap_month) 这样的语法去调用 ; 否则在编译阶段就会出现问题 , 编译提示 R.string.common_date_leap_month 不存在
但是通过上面的分析我们发现, 频繁 Crash 的版本就是因为找不到 common_date_leap_month 这个字段才 Crash 的, 既然找不到那也应该编译不过才对, 但是既然我们拿到了 apk, 那说明编译也是没问题的.
这种情况出现的话, 一般有下面两种情况
猜测三星这次出问题的是因为第二种情况, 主项目和子 modules 使用了不同版本的包, 导致可以编译通过, 但是最终打包进项目的并不是编译时候的包, 就出现了运行时的 FATAL EXCEPTION : NoSuchFieldError ( 如果有知道具体原因的可以留言讨论一波 )
开个玩笑, 这个问题对三星来说绝对是一个大的事故, 不过也贡献了一个经典的案例, 估计以后其他 App 或者手机厂商都会把这个纳入到功能测试中. 至于三星, 国内市场本身就不行了, S20 系列刚有些回暖, 又出现这档子事, 还是那句话 : 这是命, 得认, 道歉 , 不丢人
想必三星对这一天也会铭心刻骨
上面的分析过程虽然比较简单, 但是有一些比较繁琐的工作, 比如找版本, 反编译 , 看代码逻辑等. 最终也算是找到了问题的根本原因 : 编译导致的问题碰上十几年才遇到一次的闰四月. 那么从这件事我们学到了什么呢?
一个人可以走的更快 , 一群人可以走的更远
]]>不过双方斗争的受害者无疑还是使用手机的消费者 , App 如果斗争成功, 那么手机上各种后台进程乱跑, 杀不掉, 占用 CPU 和内存 , 这不是消费者想看到的 ; 如果 Rom 开发者斗争成功 , App 的体验必定会大打折扣 , 各位 App 开发者应该深有体会.
从文章最后一段可以看到, 其实各个手机厂商对付这一套都有自己的策略, 基本上都可以搞定自启动和关联启动. 至于隐私 , 李彦宏曾经说过 “中国人对隐私问题的态度更加开放,也相对来说没那么敏感。如果他们可以用隐私换取便利、安全或者效率。在很多情况下,他们就愿意这么做“ . 大家想想在微信里面复制一段话打开到淘宝就可以自动跳转到这个物品, 方不方便? 好不好用? 还想不想用? 剪贴板再借我看一看?
希望大家在隐私问题上不要打哈哈, 技术是把双刃剑, 如果隐私落到别有用心的人手上, 后果是很严重的, 就算不是为了自己, 为了下一代. 欧盟为什么要搞《通用数据保护条例》(General Data Protection Regulation,简称 GDPR), 就是为了隐私. 举个例子 , 国内很多厂商的产品现在要区分是否在欧盟买, 如果是在欧盟卖的话, 就得把里面那些收集用户数据的功能都关掉 , 否则抓住了就能罚你罚到吐血 . 至于中国和印度, 随便收集.
本篇文章不涉及到隐私部分, 我是对隐私保护无条件支持的 . 这里只从技术的角度 , 来讲一下 MIUI 12 爆出来的应用自启动和关联唤醒的问题.
PS: 大家在自己的手机上可能看不到我列的一些例子, 是因为我是用的 Android 10 的 AOSP 代码, 大部分的国产 Rom 都已经阻断了应用的这些行为.
首先解释几个技术名词, 方便大家对号入座
在 Android 中 , 一个 App 包含六部分, 进程(必选) + Activity (可选) + BroadcastReceiver (可选)+Service (可选)+ContentProvider (可选) + 子进程(可选)
一个必选项加五个可选项, 组成了一个 App , 其中 Activity(可选) + BroadcastReceiver(可选)+Service(可选)+ContentProvider(可选) 这四个又称为 Android 的四大组件, 之所以这四兄弟这么特殊, 是因为这四个组件都可以单独启动,
但是这四兄弟启动之前, 系统都会检查对应的进程是否存在, 如果进程不存在 , 那么就需要先启动进程, 再启动这个组件. 我们在桌面上点击一个应用图标, 其实启动的就是他的 Activity , 系统会先创建进程, 然后再启动 Activity , 我们才可以看到对应的界面
一般自启动和关联启动, 一般不会直接启动 Activity , 因为 Activity 是用户可感知的 , 你在后台莫名其妙起了一个界面到前台, 用户分分钟卸了你 . 所以一般自启动和关联启动都是在 BroadcastReceiver (可选) + Service (可选) + ContentProvider (可选) 三个上面做文章.
自启动指的是不借助其他的应用, 通过监听系统的一些事件, 或者文件变化, 通过系统的机制, 把自己的进程拉起来处理事情.
关联启动指的是借助其他应用来启动自己, 比如大家列出来的起点读书启动作家助手\电信营业厅\百词斩这种.
启动阻断也叫切断唤醒, Rom 开发人员在四大组件启动的地方加入逻辑判断, 符合条件的组件才能拉起自己的进程 , 不符合条件的组件直接返回 , 这样就达到了启动阻断的目的.
当然这里面还有很多工作要做, 比如工作状态判断, 拉起合理性判断 , 一旦错误的阻断必然会引起用户的使用逻辑的断裂, 比如用户在一个 App 里面要拉起支付宝进行支付 , 结果启动支付宝的支付组件的时候被你给阻断了, 可以想象用户的愤怒
有了上面几个简单的概念, 下面我们就简单说一下自启动和关联启动的技术分析 .
要分析应用启动,首先需要安装大量应用,然后执行 Monkey,让大部分进程都跑起来。我使用的 Monkey 命令如下,跑完就自己去睡觉了
1 | adb shell monkey --kill-process-after-error --ignore-security-exceptions --ignore-crashes --pct-appswitch 90 --pct-touch 10 --throttle 10000 --ignore-timeouts --ignore-native-crashes 100000000 |
首先可以用 EventLog 来查看进程的启动信息,EventLog 会如实记录每个进程的启动、死亡信息。我使用下面的命令来进行进程启动和死亡的过滤
1 | adb logcat -b events | egrep "am_proc_died|am_proc_start" |
这里主要是使用了 Dumpsys activity ,主要是用来分析进程的各个组件的信息
1 | adb shell dumpsys activity |
自启动指的是不借助其他的应用, 通过监听系统的一些事件, 或者文件变化, 通过系统的机制, 把自己的进程拉起来处理事情. 这些系统的事件就包括开机广播 / 网络变化 / 媒体库扫描等(这里只列了一部分) .
用户重启手机后, 系统会向注册了开机广播的应用发广播, 收到广播的应用就可以把自己拉起来, 开始处理对应的逻辑(拉起更多的进程) , 对应的广播如下:
1 | android.intent.action.BOOT_COMPLETED |
应用可以监听这个广播, 在用户重启手机后, 将自己唤醒, 处理自己的逻辑 , 比如说继续图片备份/ 继续同步联系人 / 检查是否有固件更新 / 推送最新的新闻等操作
当然监听这个广播也是应用自启动的一个手段
典型的广播接受处理记录 : com.tencent.news 的 com.tencent.news.system.BootBroadcastReceiver 组件接收了 android.intent.action.BOOT_COMPLETED 广播 ,处理了 7s ,至于怎么处理, 当然是先把 com.tencent.news 这个进程拉起来, 然后执行 BootBroadcastReceiver 的 onReceive 方法 . 这是一个典型的自启动的例子
网络变化包括网络连接 / 断开 / wifi 移动网络切换等操作 , 一旦发生这些事件, 系统会向对应注册了这个事件的应用发送广播 . 对应的广播如下:
1 | android.net.conn.CONNECTIVITY_CHANGE |
应用就可以监听这个广播来执行对应的逻辑 , 比如你在看直播 ,突然 wifi 断了切换成了 4G 网络, 应用就可以提醒用户是否使用移动网络继续观看, 毕竟网络直播还是很耗流量的.
当然监听这个广播也是应用自启动的一个手段
下图可以看到五个监听了网络变大的广播接收器 (只显示了五个 , 其实有 200 多个) , 监听到网络变化后拉起自身
图中 packageName 就是对于的应用的包名, name 是启动的组件
1 | android.net.wifi.STATE_CHANGE |
系统监听到文件变化或者存储盘变化也会发通知给各个应用 , 比如说增加了一个图片或者文档 , 其对于的广播如下
1 | android.intent.action.MEDIA_SCANNER_STARTED |
当然监听这个广播也是应用自启动的一个手段
下面是一个典型的监听媒体库扫描广播进行自启动的案例:
com.jd.jrapp 的广播接收器 com.jd.jrapp.library.longconnection.receiver.BootReceiver 监听到 android.intent.action.MEDIA_SCANNER_STARTED 广播后, 启动自己进程开始处理
个推是各个应用接入的一个三方 SDK , 用于消息推送 , 但其实个推也集成了上面说的哪几种自启动的方式 , 包括 BOOT_COMPLETED,CONNECTIVITY_CHANGE,USER_PRESENT 这些
关于个推,由于可定制型比较强, 比如 在项目源码中添加一个继承自 com.igexin.sdk.PushService 的自定义 Service 就可以 , 所以从 EventLog 和 Dumpsys 没法直接看出来哪个用了个推来保活或者相互唤醒, 不过其对于的子进程得设置为 :pushservice , 可以根据这个做判断(有可能不准)
所以我们直接看个推的配置文档
1 | <service |
像最前面同提到的 BootComplete , com.ss.android.ugc.aweme:pushservice 可能就是接入了个推
关联启动指的是借助其他应用来启动自己 , 比如说很多 App 接入了同一个 SDK , 那么一旦你启动了接入这个 SDK 的应用 ,那么这个 SDK 就可以启动同样接入了这个 SDK 的其他应用, 达到关联唤醒的目的
这个 SDK 可以是 BAT 集团内部自研的通用 SDK , 也可以是三方提供的 SDK , 根据我自己的调试来看 , 大家提到的 xxx 启动了 xxx , 大部分都是通过三方 SDK 来实现的 , 大部分是用了极光推送.
下面就以一个案例来看极光推送是怎么利用一个已经启动的 App 来启动另外一个没有启动的 App 的.
首先我们看 EventLog 可以看到进程的启动信息 , 包括进程名, 进程 pid , 启动的组件, 启动的组件类型.
1 | [0,19428,10195,com.qq.reader,service,{com.qq.reader/cn.jpush.android.service.DaemonService}] |
上面这条 Log 解释一下就是
执行 adb shell am force-stop com.qq.reader , 强制杀掉 QQ 阅读 , 观察 EventLog, 从下面的可以看到 , QQ 阅读的进程被拉起, 拉起的是 Service 这个组件, 其具体的内容是 com.qq.reader/cn.jpush.android.service.DaemonService
当然从 Event Log 里面我们看不出来是谁拉起了这个 Service ,这时候就需要 dumpsys activity 的帮助了, 由于是 Service 组件被拉起, 那么我们可以看 com.qq.reader 的 ServiceRecord , 其内容如下, 可以看到其 Connections 一栏, 是被 com.qidian.QDReader:pushcore 这个进程拉起的
那么对应的 , 在小米的 MIUI 12 关联启动界面就会显示 : 起点读书 在 8:48 分拉起了 QQ 阅读(由于没有小米手机, 所以没法截图, 大家自己看高票答案 https://www.zhihu.com/question/391494145 自己脑补一下就可以了)
极光推送的官方文档其实也说的很清楚, 提供了被拉起和拉起别人的功能, 看你自己怎么用.
最前面的有说到, 进程管理是应用开发者和 Rom 开发者斗争最激烈的部分 , MIUI 选择了将斗争的过程展示给了普通消费者, 让普通消费者也知道了这场斗争的细节 . 其他的厂商也做了相同的事情 , 否则整个系统基本上是没法用的 , 就像我手上现在这台测试用的 pixel , 不断有进程因为整机内存太小被 LMK 杀掉, 然后马上被各种手段重新启动 , 耗电极快, 卡的连娘都不认识了.
我们从极光和个推的官方文档就可以看到各个手机厂商的应对方法和开关的界面, 这里列出来是方便大家进去看一下, 因为各个手机厂商的白名单配置不一样, 或者有时候用户自己改过但是忘记了 , 都可以进去重新设置一下 , 对于那些你退出了就不想让他继续活动的应用 ,果断去掉白名单.
一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
这里直接上官方对于 SurfaceFlinger 的定义
—- 引用自SurfaceFlinger 和 Hardware Composer
下面是上述流程所对应的流程图, 简单地说, SurfaceFlinger 最主要的功能:SurfaceFlinger 接受来自多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。
那么 Systrace 中,我们关注的重点就是上面这幅图对应的部分
这四部分,在 Systrace 中都有可以对应的地方,以时间发生的顺序排序就是 1、2、3、4,下面我们从 Systrace 的这四部分来看整个渲染的流程
关于 App 部分,其实在Systrace 基础知识 - MainThread 和 RenderThread 解读这篇文章里面已经说得比较清楚了,不清楚的可以去这篇文章里面看,其主要的流程如下图:
从 SurfaceFlinger 的角度来看,App 部分主要负责生产 SurfaceFlinger 合成所需要的 Surface。
App 与 SurfaceFlinger 的交互主要集中在三点
关于这部分内容可以查看 Android 基于 Choreographer 的渲染机制详解 这篇文章,App 和 SurfaceFlinger 的第一个交互点就是 Vsync 信号的请求和接收,如上图中第一条标识,Vsync-App 信号到达,就是指的是 SurfaceFlinger 的 Vsync-App 信号。应用收到这个信号后,开始一帧的渲染准备
dequeue 有出队的意思,dequeueBuffer 顾名思义,就是从队列中拿出一个 Buffer,这个队列就是 SurfaceFlinger 中的 BufferQueue。如下图,应用开始渲染前,首先需要通过 Binder 调用从 SurfaceFlinger 的 BufferQueue 中获取一个 Buffer,其流程如下:
App 端的 Systrace 如下所示
SurfaceFlinger 端的 Systrace 如下所示
queue 有入队的意思,queueBuffer 顾名思义就是讲 Buffer 放回到 BufferQueue,App 处理完 Buffer 后(写入具体的 drawcall),会把这个 Buffer 通过 eglSwapBuffersWithDamageKHR -> queueBuffer 这个流程,将 Buffer 放回 BufferQueue,其流程如下
App 端的 Systrace 如下所示
SurfaceFlinger 端的 Systrace 如下所示
通过上面三部分,大家应该对下图中的流程会有一个比较直观的了解了
BufferQueue 部分其实在Systrace 基础知识 - Triple Buffer 解读 这里有讲,如下图,结合上面那张图,每个有显示界面的进程对应一个 BufferQueue,使用方创建并拥有 BufferQueue 数据结构,并且可存在于与其生产方不同的进程中,BufferQueue 工作流程如下:
上图主要是 dequeue、queue、acquire、release ,在这个例子里面,App 是生产者,负责填充显示缓冲区(Buffer);SurfaceFlinger 是消费者,将各个进程的显示缓冲区做合成操作
从最前面我们知道 SurfaceFlinger 的主要工作就是合成:
当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。
其 Systrace 主线程可用看到其主要是在收到 Vsync 信号后开始工作
其对应的代码如下,主要是处理两个 Message
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
1 | void SurfaceFlinger::onMessageReceived(int32_t what) NO_THREAD_SAFETY_ANALYSIS { |
handleMessageRefresh 中按照重要性主要有下面几个功能
由于显示系统有非常庞大的细节,这里就不一一进行讲解了,如果你的工作在这一部分,那么所有的流程都需要熟悉并掌握,如果只是想熟悉流程,那么不需要太深入,知道 SurfaceFlinger 的主要工作逻辑即可
通常我们通过 Systrace 判断应用是否掉帧的时候,一般是直接看 SurfaceFlinger 部分,主要是下面几个步骤
关于这一部分的 Systrace 怎么看,在 Systrace 基础知识 - Triple Buffer 解读-掉帧检测 部分已经有比较详细的解读,大家可以过去看这一段
关于 HWComposer 的功能部分我们就直接看 官方的介绍 即可
——– 引用自SurfaceFlinger 和 Hardware Composer
我们继续接着看 SurfaceFlinger 主线程的部分,对应上面步骤中的第三步,下图可以看到 SurfaceFlinger 与 HWC 的通信部分
这也对应了最上面那张图的后面部分
不过这其中的细节非常多,这里就不详细说了。至于为什么要提 HWC,因为 HWC 不仅是渲染链路上重要的一环,其性能也会影响整机的性能,Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章里面就有列有 HWC 导致的卡顿问题(性能不足,中断信号慢等问题)
想了解更多 HWC 的知识,可以参考这篇文章Android P 图形显示系统(一)硬件合成HWC2,当然,作者的Android P 图形显示系这个系列大家可以仔细看一下
一个人可以走的更快 , 一群人可以走的更远
原文比较简单,并没有介绍为什么要推荐这些,只是单纯地列了一下知识点,我这边针对每个知识点做一些简单的介绍,有些知识点原文并没有提到,我会根据自己的理解加上,仅供参考
这篇文章主要针对 Android 开发者,如果你是新手,那么下面的内容可以帮助你找到学习的线路;如果你是老手,这篇文章列出的内容也可以帮助你查漏补缺。如果各位有什么其他的建议,欢迎留言交流
Java 是 Android App 开发默认的语言, Android Framework 也是默认使用 Java 语言,熟练掌握 Java 语言是 Android 开发者的必备技能。
希望深入 Java 虚拟机的同学,也可以参考下面两本书:
Google 几年前就开始走 “Kotlin First” 的路线,目前很多官方的文档和 Demo 都是使用 Kotlin 语言作为默认,Kotlin 的重要性不言而喻。
Google 官方也出了个“Refactoring to Kotlin”的教程,其介绍如下:
此 Codelab 的适用对象为任何使用 Java 并考虑将其项目迁移到 Kotlin 的开发者。我们将从数个 Java 类入手,引导您使用 IDE 将它们转换为 Kotlin。接着,我们会审视转换后的代码,研究如何加以改善,使其更符合使用习惯,同时避免常见错误
Flutter 作为 Google 的亲儿子,其官方的扶持力度大家有目共睹。 Flutter 于几天前发布了v1.12.13_hotfix.7 版本,修复了几个比较严重的 Bug,如Flutter 1.12 最新 hotfix 与 2020 路线计划 这篇文章介绍所述,“v1.12.13+hotfix.7 版本主要在于解决了我比较关心的三个问题,包括: reportFullyDrawn 异常、华为手机上崩溃、光标和键盘输入异常 这几个问题。”.感兴趣也可以看一下其 1 月 30 号发布的 2020 Roadmap
Flutter 的发展大家可以看一下 Gityuan 的这一篇Flutter 跨平台演进及架构开篇,目前字节跳动的多个 App 已经接入 Flutter 进行混合开发。个人对 2020 年 Flutter 不再持观望态度,读者可以根据自己的技术规划决定是否开始学习
Android Studio 作为 Android 默认的开发者工具,目前的版本更新已经解决了诸多之前的性能问题,虽然目前对硬件资源的要求仍然比较高,但是一旦你接受了这个设定,真香预警!
AS 主要需要熟悉下面几点
熟悉各种项目的目录结构,资源文件、Gradle 文件
这部分不必做过多的解释,下面列出的就是大家熟悉的 Android 四大组件,Android 开发的基础
Android 默认的布局很多时候都没法满足设计的需求,这时候就需要自定义 View,你需要掌握下面几个知识点的使用
相比 HardCode,使用资源文件会让代码的可修改性更高
许多人提倡 App 使用 单 Activity + 多个 Fragment 的组合,可见 Fragment 在开发中的重要性,但是 Fragment 的管理又是一门技术,Fragment 的坑,只能在实际开发中慢慢填平了,不过下面的 Fragment 基础还是要牢固
这里列的同样是一些功能组件,需要知道这是什么东西,基本的用法
App 开发不免要和文件打交道,文件的读写、存储都是必不可少的,下面列出了几种 Android 中存储相关的知识点
Android App 默认使用 Gradle 进行编译,关于 Gradle 的使用必须要熟悉,以及如何区分开发版本和 Release 版本,以及国内特有的多渠道打包技术、以及 ASM 等
理解 Thread 非常重要,Android App 只有一个主线程,其余的我们称之为工作线程,我们的很多工作需要再工作线程和主线程直接切换,如何高效创建和释放线程、线程池、线程间通信、Message-Looper-Handler 模型这些知识点都要了熟于心,另外进阶的话 Binder 通信也是需要掌握的知识
这里列举了一些 Debug 的基本手段,实际开发中遇到具体问题的时候一般都会用到,不过有的可能入手难度要高一些,需要花时间去掌握。Debug 工具除了下面这几个还有很多
内存泄漏是一个很大的专题,包括 Java 内容泄漏和 Native 内存泄漏,涉及的知识点非常多,可以单独拿出来做一个大的知识栈。一般来说, Java 内存泄漏会比较好检测和修复,但是 Native 内存泄漏就会比较难。
经典的第三方类库,可以大幅节约我们的开发时间
常见的一些数据保存流格式
Jetpack 是 Google 推出的一套库、工具和指南,可帮助开发者更轻松地编写优质应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上。Jetpack 包含与平台 API 解除捆绑的 androidx.* 软件包库。这意味着,它可以提供向后兼容性,且比 Android 平台的更新频率更高,以此确保您始终可以获取最新且最好的 Jetpack 组件版本。
传统的开发架构,没有绝对的哪个好哪个不好,只有哪个适合哪个不适合,下面三种你都应该知道并有一定的了解
Firebase 国内很多开发者用不到,这里简单看一下即可(说不定哪天国内就可以用了呢)
安全方面接触毕竟多的应该是加密、解密、混淆等,毕竟用户数据安全大于一切,不重视这个欧盟会教你做人
应用发布相关的知识,国内还得加上多渠道打包、插件化
作为一个有进取心的 Android 开发者,拥有自己的技术栈和规划非常重要,技术栈确保你有足够的市场竞争力,从而形成护城河;技术规划则可以给你一个明确的学习目标。卸载抖音、微博、斗鱼、游戏吧,做好 2020 年的规划,Keep Learning and Improving ,共勉
如果你苦于没有好的时间管理方法,可以参考这个视频我是怎么做周计划 | 生产力提升 | 我的方法,这个是我熟悉的一个大佬的工作学习方法实践,推荐给大家
凡是预则立,不预则废,年度计划太长,日计划又太短。实践下来发现以周为单位做时间管理(时间管理)最靠谱,既考虑了短期又考虑了长期,可以使自己长期坚持做某事,也有一定的时间长度用来甄有价值的事情。
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
一个人可以走的更快 , 一群人可以走的更远
]]>我比较喜欢读历史相关的书,其中 汴京之围 这本书读起来非常难受,弱国无外交,真的是写实;坏小孩 书如其名,个人感觉是要比电视剧好很多;重来 2 和 重来 3 跟工作相关,讲的是远程工作和工作方法,跟 2020 年居家办公的大趋势一致,白领程序员推荐看一下;我的最后一本减肥书 则比较系统和专业地讲了一下减肥相关的知识,对于在减肥的我来说很有用;最后 镖人 也算是国漫经典了,等出完了之后我一定要买一套实体书典藏(巨人也是)
2020 读书不多,2021 要加大这方面的投入,读 + 记录 + 总结 ,读书笔记后续都会补上
目前更新的书单
首先介绍一下作者,这样大家读书的时候,就会有一定的带入感
吴晗,原名吴春晗,字辰伯。历史学教育家。浙江义乌人。1957 年加入中国共产党。1934 年毕业于清华大学。后任云南大学、西南联合大学教授,清华大学教授、系主任、文学院院长。1943 年参加中国民主政团同盟,积极从事民主运动。建国后,历任北京市副市长、北京市第一至四届政协副主席、中国科学院哲学社会科学部委员。1958 年当选为民盟中央副主席。是第一至三届全国人大代表,第一届全国下和协委员,第二、三届全国政协常委。生平从事中加古代史研究,对明史的厂家尤有成就。着有《朱元璋传》和历史剧《海瑞罢官》等。「文化大革命」开始后,吴晗从精神到肉体惨遭摧残,随后于 1968 年 3 月被捕入狱,1969 年 10 月 11 日被迫害致死。「文化大革命」结束后,其冤案才得以平反昭雪。
这本明朝历史的教训是一本文章集,内容包括朱元璋的统治之术、统治阶级的内部矛盾、东西厂和锦衣卫、国民生活、党争、农民、奴隶和兵变、仕宦阶级、流寇等。如果说「明朝那些事」讲的是历朝大人物的事情,那么这本书则从另外一个角度,带我们了解明朝从统治阶级到被统治阶级的各种乱象,很多章节互相独立却又互相联系,可以说晚明时期的内忧外患,并非简单的清人入侵和流寇四起这么简单,这个朝代从顶到根就已经坏了,不是一两个人能救得来的
这本书微信读书上有,推荐。
你肯定听过「靖康之耻」,本书就是详细描写靖康之耻产生的前因后果,说实话看之前并没有对这段历史有详细的了解,看了之后才觉得,叫靖康之耻真的是一点没错,尤其是军队一碰就没,谈判背信弃义,围城大半年对首都汴京一遍又一遍的搜刮,看着又气又无可奈何,没有实力就会被欺负,这个亘古不变的真理,历史上一次又一次的实践,从盛世到灭亡只花了三年……文中有一段话我很喜欢「它提醒我们居安思危,在任何时候,危机和盛世只差一步而已。和平并不是一种必然,它要求我们怀着谦卑的心态去看待世界,学习世界所长的同时,避免自大与狂傲。更重要的是,必须有意识地避免战争,谦卑不是错,错判了形势才是最可怕的,因为任何形势都是环环相扣的,一旦迈出了第一步,不仅无法回头,而且也无法把握未来的走向了」
本书追溯北宋末年靖康之难的完整历史细节,讲述宋、辽、金三方的和与战,聚焦北宋历史大变局的关键时刻,以及帝国由内而外全局性危局大爆发的前因后果。北宋宣和年间,帝国上下一片繁荣景象,然而盛世之下的隐患已成暗涌。财政困难、军事痼疾、恶性党争等内部危机,北方辽、金两国的军事威胁等外部危机,使得帝国渐成风雨飘摇之势。为「收复」作为战略屏障的燕云十六州,宋徽宗决定联金灭辽。宋金联盟虽然逐渐将辽国蚕食,但金国借此窥见北宋的虚弱,加之两国复杂的利益纠纷,金国转而南下攻宋。靖康元年(1126),金军第二次围攻汴京,十一月汴京城陷。北宋轰然崩溃,从盛世到灭亡仅隔三年。作者以兼顾宋、辽、金三方立场的史料记载为基础,用通俗流畅的叙事笔法,试图复盘靖康之难历史发生的过程,探寻北宋盛衰之变背后的深层成因,以及超越时代的镜鉴意义。
这本书微信读书上有,推荐。
《Android 高效进阶:从数据到 AI》是一本 Android 进阶技术与实践应用相结合的书籍,主要从 3 个方面来组织内容。第一个方面,Android 工程构建体系实践与进阶,其中不仅包含了移动数据技术、工具基建进阶、效能进阶,还包含了工具应用进阶、工程构建进阶等内容;第二个方面,对当前移动端前沿技术的探索,包含容器技术、大前端技术和 AI 技术;第三个方面,移动应用的安全攻防技术和设计模式进阶实践。本书内容全面,侧重实战经验和进阶技能,通过本书不仅能学到最新的移动端技术,以及进阶技术与实践应用相结合的知识,更重要的是能领悟到作者对技术的钻研精神和思维方式,从而帮助 Android 开发者高效进阶
《Android 高效进阶:从数据到 AI》适合移动应用开发者、Android 系统开发人员、Android 系统安全工程师,以及 Android 领域的移动技术负责人阅读
从书的内容来看,作者是一名经验丰富的开发者,很多专题都有比较详细的架构实现,读者可以根据这些架构实现一套,是一本不可多得的经验之书,这本书在微信读书可看
作者吴晓波从商业的角度,来展示了近代一百年来的中国的发展。作者提到发展中的三个现象::一是意识形态争论对现代化的干扰,二是中央集权观念对国家商业主义的催生,三是传统的轻商和官商文化对新生企业家阶层的影响。让人叹息的是,在洋务运动后的多次经济变革运动中,这三个命题都幽灵般地随影而至,无法摆脱。
读完个人感受是:国家稳定对商业发展太重要的,我国近代起政局非常不稳定,商业的发展总是随着政局的变化而兴起和衰落;改革开放前的商业基本上停滞了。可以说我国的商业和科技发展是从改革开放后基本上从 0 开始发展的,与稳定发展了一百多年的美国的差距是非常巨大的,这一点是我看完这本书最大的感慨,虽然存在技术共享和技术爆炸,但是我们国家掌握的核心技术还是太少了。
在这100多年的时间里,苦难让我们有机会凝神思索,学到不少东西。它使中国人得以细细体察所历之事,对千年历史进行更严苛的观察,若非受辱,我们对之也许根本不会留心,还沉浸在骄傲的大国幻境之中。
自1840年鸦片战争之后,有一个词汇覆盖了所有的主题,它成为无数热血国人的毕生理想,这就是“强国”。“强国”的急迫,让这个国家变得无比的焦虑,有时候甚至显得迫不及待,在一条道路还没有完全考察清楚的时候,便不惜铤而走险。在很多敏感关键的时刻,渐进式的思想往往被视为“反动”,颠覆式革命,甚至流血暴力,成为全民性的选择。百年春秋,闹剧、悲剧与喜剧交织上演。
在这个被“强国梦”激励着的100年里,中国的复兴开始于一个幽暗而绝望的梦醒时刻。商业的演进一直是国家进步和民族雪耻的重要方向,正是在这一进程中,新兴的企业家阶层崛起为一支独立的力量。而他们的曲折命运又与这个国家的政治变革和全民抉择纠缠在一起,它们时而合一,时而决裂,却在绝大多数时间里处在不和谐的状态中。百年以来,中国经济的问题,归根到底可以总结为三个利益关系的调整:一是政府利益与公众利益的调整,二是中央政府与地方政府利益的调整,三是富裕公众与贫穷公众的利益调整。作为富裕公众的代表阶层,企业家集团在与政府(包括中央政府及地方政府)、知识分子和贫穷公众的关系相处上,一直没有达成原则性和建设性的共识,这也成为中国商业进步总是被各种事件打断的重要原因之一。
这本书叫 Remote, 国内翻译成了重来2, 简直是瞎比翻译.
本书作者是 37Signals 两位创始人, 书中介绍了远程工作的方方面面, 包括远程工作的优点、远程工作中如何协作、远程工作的副作用、远程工作中的员工管理、远程工作中的自我管理等. 2020 年的一场新冠病毒, 使得大部分公司都尝试了一把远程工作, 这本书中所提到的一些事情, 在实际远程工作中也都有遇到.
摘抄其中一些, 大家可以品品
真正重要的是把工作做好,而不是死守着上下班时间
新时代的奢侈就是摆脱“日后再享受生活”的思维桎梏,现在就去做你热爱的事,跟工作并行。何必要把时间浪费在那种“等我退休了,生活该有多美好”的白日梦上?在工作跟退休之间划一道界限,这其实是相当武断的。你的人生无须再遵循这样的规则。你可以把这两样混合在一起,既有趣又有钱挣——设计一种更好的、能把工作变得有趣的生活方式,因为工作不是这辈子唯一的事。那副金手铐令你没法过上你真心想过的人生,快从怨恨的情绪中解脱出来吧!
当你没法整天盯着某人的时候,唯一的判断标准就是工作成果。除此之外的一大堆琐碎标准全都不见了。你只需看工作成果,因此,你不必问远程工作的员工“你今天都做了些什么”,而是说一句“把你今天的成果给我看看”就行
当开会成为常态,成为探讨、争论的必备工具,无论解决什么问题都要用一用的时候,它就被滥用了,人人都变得麻木。会议应该像盐,小心翼翼地在菜品上洒上一点儿,用于提味,而不是一勺一勺地哗哗倒上许多。盐放得太多,菜就毁了;会开得太多,人们的士气和积极性就会降低
如果你没能好好地掌握生活与工作的平衡,远程工作的自由就会变成奴役。这种情况是可能的,因为当你从朝九晚五的工作中解脱出来之后,很容易又会套上全天不停工作的枷锁
远程工作把罩幕揭开,让人们看到一个一直存在却并不是总被人承认或被人看到的事实:优秀的远程员工就是优秀的员工,就是这么简单
内在动力:程序员编写开源软件,一般都是因为热爱这件事,不是为了钱。钱往往会相伴而来,但它极少是动力来源。也就是说,当你在解决一个特别感兴趣的、令你兴奋激动的问题的时候,你根本不需要有管理者经常从你背后伸脖子过来看看你是否在干活。
一切公开:绝大多数开源软件都是通过邮件列表和 GitHub 这样的代码追踪系统来协调运作的。只要有人想出手帮忙,就能做到,因为所有的信息都是公开的。你可以自告奋勇地参与进来,对某块内容最精通的人很容易就能切入。
作者扶霞·邓洛普 Fuchsia Dunlop 在牛津长大,于剑桥大学取得英国文学学士学位,其后于伦敦亚非学院以名列前茅的优异成绩获得中国研究硕士学位。一九九四年,扶霞在获得了英国文化协会奖学金补助后,前往中国四川大学就读一年;其后又在四川烹饪高等专科学校接受了三个月的专业厨师训练,成为该校第一位外国学生
这本鱼翅与花椒,本来是怀着轻松的心情去读的,本书的前半部分气氛确实也是很轻松愉快,跟着作者在成都的大街小巷里面寻找家常菜,每一节之后还有一个家常菜的菜谱,作者对成都生活的描写也让人觉得惬意:
这个地方本身那种慢悠悠的倦怠感也令人不知不觉地被影响。在成都这个城市,别说实现计划了,制定计划都根本不可能。从唐朝开始,这里就以生活安逸闲适著称。因为气候适宜,土壤更是传奇般地肥沃。成都人不用特别努力地工作也能吃得好、玩儿得开心。这座城市有点南方的感觉,甚至都有点像地中海沿岸了。成都人的脚步都比北京人或上海人要慢。他们在茶馆里一坐就是一下午加一晚上,打麻将、打牌、用节奏舒缓、语气甜腻的四川话开玩笑斗嘴,韵母都拖得长长的,还要加上娇俏的儿化音。他们把这叫做“摆龙门阵”,四川特有的谈天说地。而四川话里最生动的一句方言莫过于“好耍(特别有趣)”。他们说的时候总是懒洋洋的声气,咧嘴而笑,竹椅子发着嘎吱嘎吱的背景音。“沿海的那些人,”一位出租车司机跟我聊起广东人和福建人,“他们野心大得很,也肯干,所以他们就先富起来了噻。我们四川人喃,挣的钱可以吃香喝辣就够了。”
书中描述的菜品有成都的家常菜、香港的粤菜、湖南的湘菜、扬州的江浙菜,不过由于作者四川呆的时间比较久,所以川菜和四川生活出现的更多一些,各地的美食各有特色,光看作者的描述,就能感觉到那扑鼻的香味
不过书的后半段也探讨了食品安全问题、吃野味、吃保护动物,以及中国官场的一些事情,越看越沉重,却也无能为力. 城市在变,社会在变,人也在变,事物也在变,作者再一次来到巨变后的成都,以一种旁观者的身份记录了这些变化:
一方面,这样的拆除实在是个悲剧,是我个人的悲剧:竟然爱上了一个正如此迅速地消失着的地方。我对饮食烹饪的研究,初衷是想记录一个生机勃勃的城市。后来我才明白,从很多角度来说,我都在书写老成都的“墓志铭”。我感觉这也是成都人的悲剧,虽然他们并没有意识到。这个城市是多么迷人、多么独特啊,现在要用一个中国任何地方都存在的城市取而代之,暴殄天物、可悲可叹。
另一方面,九十年代的中国似乎又洋溢着满满的生机与乐观。之前那种功利主义、禁欲主义、千篇一律的呆板与单调乏味消失不见。全国上下都在动起来,十二亿人团结一心、一致向前。在英国,哪怕拆除一栋破旧的老楼,我们都会烦恼苦闷。而在四川,他们一路挥舞大锤,把整座城市都拆平了!这无所顾忌的信心让人不得不佩服。他们坚信,未来会比过去更好。
所以,尽管经过那些被夷为平地的街道时我的心还是会痛,但同时又被这充满活力的乐观鼓动着、躁动着。我也处在一种不稳定的状态,我的人生也在改变。我在挖掘潜在的创造力、在交很棒的朋友,像一条蛇一样慢慢蜕皮。
出生不由自己,但人生可以
因为 Daily Show 所以开始关注崔娃,这本书是崔娃的自传,名字起得就很让人困惑,什么叫天生有罪?之前对于非洲历史没有什么关注, 看了这本书之后才知道,天生有罪指的是非洲的种族隔离时期,黑人和白人是不能在一起的,更别说生小孩,但崔娃就是白人父亲和黑人母亲的儿子,所以说他天生就是有罪的.
贯穿文章的两个大的主题,一个是种族隔离,一个是种族认同。非洲白人统治时期实行种族隔离,这种隔离不仅仅是人的隔离,还包括语言隔离、文化隔离、思想隔离,甚至还有黑人部落之间的隔离;另外一个就是种族认同,崔娃在当地属于有色人种,属于那种既不被黑人团体容纳,也不被白人团体容纳,印度人、中国人也会自己抱团。这时候对于一个孩子来说,跟谁抱团玩(获得哪个团队的认同)是非常重要的事情,从小崔娃就被各个团体隔离,但是他在母亲和知识中找到了在各个种族和团体之间的生存之道,最终成为一位成功的喜剧演员、脱口秀表演者、电视电台节目主持人。
种族隔离代表着一个警察国家,一个让黑人处于绝对控制下的各种法条和监视系统。若能将所有法条写下来堆到一起,那需要用掉三千多张纸,可重达五千克。但是南非种族隔离的精髓对美国人来说是非常容易理解的,在美国历史上,曾经发生过三件事:把原住民驱赶到保留地、黑人奴隶制、隔离制度。想象一下,这三件事在同一时间内发生在同一群人身上,那就是种族隔离。
]]>我找到了我的位置。既然我不属于任何一个小圈子,那么我可以在不同的圈子之间游走。我还是一条变色龙,文化上的变色龙。我知道如何去融入。我可以和爱运动的小孩一起运动,和书呆子一起讨论电脑。我可以跳进人群里,和小镇男孩一起跳舞。我可以和每个人都产生短暂的交集,一起学习、聊天、讲笑话、送餐。
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
下面是高通骁龙 845 手机 Systrace 对应的 Kernel 中的 CPU Info 区域(底下的一些这里不讲,主要是讲 Kernel CPU 信息)
Systrace 中 CPU Info 一般在最上面,里面经常会用到的信息包括:
总的来说,Systrace 中的 Kernel CPU Info 这里一般是看任务调度信息,查看是否是频率或者调度导致当前任务出现性能问题,举例如下:
与 CPU 运行信息相关的内容在 Systrace 基础知识 – 分析 Systrace 预备知识 这篇文章里面有详细的讲解,不熟悉的同学可以配合这篇文章一起食用
简单来说目前的手机 CPU 按照核心数和架构来说,可以分为下面三类:
目前的大部分 CPU 都是大小核架构,当然也有一些 CPU 是大中小核架构,比如高通骁龙 855\865,也有少部分 CPU 是非大小核架构
下面就来说说各种架构的区别,方便大家后续查看 Systrace
很早的机器 CPU 只有双核心或者四核心的时候,一般只有一种核心架构,也就是说这四个核心或者两个核心是同构的,相同的频率,相同的功耗,一起开启或者关闭;有些高通的中低端处理器也会使用同构的八核心处理器,比如高通骁龙 636
现在的大部分机器已经不使用非大小核的架构了
现在的 CPU 一般采用 8 核心,八个核心中,CPU 0-3 一般是小核心,CPU 4-7,如下图中 Systrace 中就是按照这个排列的
小核心一般来说主频低,功耗也低,使用的一般是 arm A5X 系列,比如高通骁龙 845,小核心是由四个 A55 (最高主频 1.8GHz ) 组成
大核心一般来说最高主频比较高,功耗相对来说也会比较高,使用的一般是 arm A7X 系列,比如高通骁龙 845,大核心就是由四个 A75(最高主频 2.8GHz)组成
下图就是 845 的 CPU
当然大小核架构中还有一些变种,比如高通骁龙 636 (4 小核 + 2 大核)或者高通骁龙 710 (6 小核 + 2 大核),宗旨还是不变,大核心用来支持高负载场景,小核心用来日常使用,至于够不够用,就看你舍不舍得花银子,毕竟一分价钱一分货,高通爸爸也不是做福利的
下面这些高通的主流大小核处理器的参数如下
部分 CPU 比较另辟蹊径,选择了大中小核的架构,比如高通骁龙 855 8 核 (1 个 A76 的大核+3 个 A76 的中核 + 4 个 A55 的小核)和之前的的 MTK X30 10 核 (2 个 A73 的大核 + 4 个 A53 的中核 + 4 个 A35 的小核)以及麒麟 980 8 核 (2 个 A76 的大核 + 2 个 A76 的中核 + 4 个 A55 的小核)
相比大小核架构,大中小核架构中的大核可以理解为超大核(高通称之为 Gold +) ,这个超大核的个数一般比较少(1-2 个),主频一般会比较高,功耗相对也会高很多,这个是用来处理一些比较繁重的任务
下图是 855、845 和麒麟 980 的对比
顺带提一嘴,今年的高通骁龙 865 依然是大中小核的架构,大核和中核用的是 A77 架构,小核用的是 A55,大核和中核最高频率不一样,大核只有一个,主频到 2.8GHz,不知道 865 Plus 会不会搞到 3GHz
绑核,顾名思义就是把某个任务绑定到某个或者某些核心上,来满足这个任务的性能需求:
上面是一些绑核的例子,目前 Android 中绑核操作一般是由系统来实现的,常用的有三种方法
使用 CPUset 子系统可以限制某一类的任务跑在特定的 CPU 或者 CPU 组里面,比如下面,Android 中会划分一些默认的 CPU 组,厂商可以针对不同的 CPU 架构进行定制,目前默认划分
每个 CPU 架构对应的 CPUset 的配置都不一样,每个厂商也会有不同的策略在里面,比如下面就是一个 Google 官方默认的配置,各位也可以查看对应的节点来查看自己的 CPUset 组的配置
1 | //官方默认配置 |
对应的,可以在每个 CPUset 组的 tasks 节点下面看有哪些进程和线程是跑在这个组里面的
1 | $ adb shell cat /dev/CPUset/top-app/tasks |
需要注意每个任务跑在哪个组里面,是动态的,并不是一成不变的,有权限的进程就可以改
部分进程也可以在启动的时候就配置好跑到哪个进程里面,下面是 lmkd 的启动配置,writepid /dev/CPUset/system-background/tasks 这一句把自己安排到了 system-background 这个组里面
1 | service lmkd /system/bin/lmkd |
大部分 App 进程是根据状态动态去变化的,在 Process 这个类中有详细的定义
android/os/Process.java
1 | /** |
在 OomAdjuster 中会动态根据进程的状态修改其对应的 CPUset 组, 详细可以自行查看 OomAdjuster 中 computeOomAdjLocked、updateOomAdjLocked、applyOomAdjLocked 的执行逻辑(Android 10)
使用 affinity 也可以设置任务跑在哪个核心上,其系统调用的 taskset, taskset 用来查看和设定“CPU 亲和力”,其实就是查看或者配置进程和 CPU 的绑定关系,让某进程在指定的 CPU 核上运行,即是“绑核”。
显示进程运行的CPU
1 | taskset -p pid |
注意,此命令返回的是十六进制的,转换成二进制后,每一位对应一个逻辑 CPU,低位是 0 号CPU,依次类推。如果每个位置上是1,表示该进程绑定了该 CPU。例如,0101 就表示进程绑定在了 0 号和 3 号逻辑 CPU 上了
绑核设定
1 | taskset -pc 3 pid 表示将进程pid绑定到第3个核上 |
Android 中也可以使用这个系统调用,把任务绑定到某个核心上运行。部分较老的内核里面不支持 CPUset,就会用 taskset 来设置
在 Linux 的调度算法中修改调度逻辑,也可以让指定的 task 跑在指定的核上面,部分厂家的核调度优化就是使用的这种方法,这里就不具体来讲了
正常情况下,CPU 的调度算法都可以满足日常的使用,但是在 Android 中的部分场景里面,单纯依靠调度器,可能会无法满足这个场景对性能的要求。比如说应用启动场景,如果让调度器去拉频率迁核,可能就会有一定的延迟,比如任务先在小核跑,发现小核频率不够,那就把小核频率往上拉,拉上去之后发现可能还是不够,经过几次一直拉到最高发现还是不够,然后把这个任务迁移到中核,频率也是一次一次拉,拉到最高发现还是不够,最好迁移到大核去做。这样一套下来,时间过去不少不说,启动速度也不是最快的
基于这种情况的考虑,系统中一般都会在这种特殊场景直接暴力拉核,将硬件资源直接拉到最高去运行,比如 CPU、GPU、IO、BUS 等;另外也会在某些场景把某些资源限制使用,比如发热太严重的时候,需要限制 CPU 的最高频率,来达到降温的目的;有时候基于功耗的考虑,也会限制一些资源在某些场景的使用
目前 Android 系统一般会在下面几个场景直接进行锁频(不同厂家也会自己定制)
以 高通平台为例,在 CPU Info 中我们也可以看到锁频的情况
CPU info 中还有标识 CPU 状态的标记,如下图所示,CPU 状态有 0 ,1,2,3 这四种
之前的 CPU 支持热插拔,即不用的时候可以直接关闭,不过目前的 CPU 都不支持热插拔,而是使用 C-State
下面是摘抄的其他平台的支持 C0-C4 的处理器的状态和功耗状态,Android 中不同的平台表现不一致,大家可以做一下参考
Systrace 我们一般用 Chrome 打开,转换成图形化信息之后更加方便从整体去看,但其实 Systrace 也可以以文本的方式打开,也可以看到一些详细的信息。
比如下面就是一条标识 CPU 调度的 Message,解析的时候,里面的信息会被解析到各个模块
1 | appEventThread-8193 [001] d..2 1638545.400415: sched_switch: prev_comm=appEventThread prev_pid=8193 prev_prio=97 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120 |
详细来看
1 | appEventThread-8193 -- 标识 TASK-PID |
另外里面仔细看也可以看到许多有趣的输出,可以加深对调度的理解
一个人可以走的更快 , 一群人可以走的更远
拍了张照片,觉得还不错,分享给大家
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
Systrace 中可以看到应用的掉帧情况,我们经常看到说主线程超过 16.6 ms 就会掉帧,其实不然,这和我们这一篇文章讲到的 Triple Buffer 和一定的关系,一般来说,Systrace 中我们从 App 端和 SurfaceFlinger 端一起来判断掉帧情况
如果之前没有看过 Systrace 的话,仅仅从理论上来说,下面这个 Trace 中的应用是掉帧了,其主线程的绘制时间超过了 16.6ms ,但其实不一定,因为 BufferQueue 和 TripleBuffer 的存在,此时 BufferQueue 中可能还有上一帧或者上上一帧准备好的 Buffer,可以直接被 SurfaceFlinger 拿去做合成,当然也可能没有
所以从 Systrace 的 App 端我们是无法直接判断是否掉帧的,需要从 Systrace 里面的 SurfaceFlinger 端去看
SurfaceFlinger 端可以看到 SurfaceFlinger 主线程和合成情况和应用对应的 BufferQueue 中 Buffer 的情况。如上图,就是一个掉帧的例子。App 没有及时渲染完成,且此时 BufferQueue 中也没有前几帧的 Buffer,所以这一帧 SurfaceFlinger 没有合成对应 App 的 Layer,在用户看来这里就掉了一帧
而在第一张图中我们说从 App 端无法看出是否掉帧,那张图对应的 SurfaceFlinger 的 Trace 如下, 可以看到由于有 Triple Buffer 的存在, SF 这里有之前 App 的 Buffer,所以尽管 App 测一帧超过了 16.6 ms, 但是 SF 这里依然有可用来合成的 Buffer, 所以没有掉帧
上面的掉帧我们是从渲染这边来看的,这种掉帧在 Systrace 中可以很容易就发现;还存在一种掉帧情况叫逻辑掉帧
逻辑掉帧指的是由于应用自己的代码逻辑问题,导致画面更新的时候,不是以均匀或者物理曲线的方式,而是出现跳跃更新的情况,这种掉帧一般在 Systrace 上没法看出来,但是用户在使用的时候可以明显感觉到
举一个简单的例子,比如说列表滑动的时候,如果我们滑动松手后列表的每一帧前进步长是一个均匀变化的曲线,最后趋近于 0,这样就是完美的;但是如果出现这一帧相比上一帧走了 20,下一帧相比这一帧走了 10,下下一帧相比下一帧走了 30,这种就是跳跃更新,在 Systrace 上每一帧都是及时渲染且 SurfaceFlinger 都及时合成的,但是用户用起来就是觉得会卡. 不过我列举的这个例子中,Android 已经针对这种情况做了优化,感兴趣的可以去看一下 android/view/animation/AnimationUtils.java 这个类,重点看下面三个方法的使用
1 | public static void lockAnimationClock(long vsyncMillis) |
Android 系统的动画一般不会有这个问题,但是应用开发者就保不齐会写这种代码,比如做动画的时候根据**当前的时间(而不是 Vsync 到来的时间)**来计算动画属性变化的情况,这种情况下,一旦出现掉帧,动画的变化就会变得不均匀,感兴趣的可以自己思考一下这一块
另外 Android 出现掉帧情况的原因非常多,各位可以参考下面三篇文章食用:
首先看一下 BufferQueue,BufferQueue 是一个生产者(Producer)-消费者(Consumer)模型中的数据结构,一般来说,消费者(Consumer) 创建 BufferQueue,而生产者(Producer) 一般不和 BufferQueue 在同一个进程里面
其运行逻辑如下
Android 通过 Vsync 机制来控制 Buffer 在 BufferQueue 中的流动时机,如果对 Vsync 机制不了解,可以参考下面这两篇文章,看完后你会有个大概的了解
上面的流程比较抽象,这里举一个具体的例子,方便大家理解上面那张图,对后续了解 Systrace 中的 BufferQueue 也会有帮助。
在 Android App 的渲染流程里面,App 就是个生产者(Producer) ,而 SurfaceFlinger 是一个消费者(Consumer),所以上面的流程就可以翻译为
理解了 BufferQueue 的作用后,接下来来讲解一下 BufferQueue 中的 Buffer
从上面的图可以看到,BufferQueue 中的生产者和消费者通过 dequeueBuffer、queueBuffer、acquireBuffer、releaseBuffer 来申请或者释放 Buffer,那么 BufferQueue 中需要几个 Buffer 来进行运转呢?下面从单 Buffer,双 Buffer 和 Triple Buffer 的角度分析(注意这里只是从 Buffer 的角度来做分析的, 比如 App 测涉及到 Buffer 的是 RenderThread , 不过由于 RenderThread 与 MainThread 有一定的联系, 比如 unBlockUiThread 执行的时机, MainThread 也会因为 RenderThread 执行慢而被 Block 住)
单 Buffer 的情况下,因为只有一个 Buffer 可用,那么这个 Buffer 既要用来做合成显示,又要被应用拿去做渲染
理想情况下,单 Buffer 是可以完成任务的(有 Vsync-Offset 存在的情况下)
但是很不幸,理想情况我们也就想一想,这期间如果 App 渲染或者 SurfaceFlinger 合成在屏幕显示刷新之前还没有完成,那么屏幕刷新的时候,拿到的 Buffer 就是不完整的,在用户看来,就有种撕裂的感觉
当然 Single Buffer 已经没有在使用,上面只是一个例子
Double Buffer 相当于 BufferQueue 中有两个 Buffer 可供轮转,消费者在消费 Buffer的同时,生产者也可以拿到备用的 Buffer 进行生产操作
下面我们来看理想情况下,Double Buffer 的工作流程
但是 Double Buffer 也会存在性能上的问题,比如下面的情况,App 连续两帧生产都超过 Vsync 周期(准确的说是错过 SurfaceFlinger 的合成时机) ,就会出现掉帧情况
Triple Buffer 中,我们又加入了一个 BackBuffer ,这样的话 BufferQueue 里面就有三个 Buffer 可以轮转了,当 FrontBuffer 在被使用的时候,App 有两个空闲的 Buffer 可以拿去生产,就算生产过程中有 GPU 超时,CPU 任然可以拿到新的 Buffer 进行生产(即 SurfaceFling 消费 FrontBuffer,GPU 使用一个 BackBuffer,CPU使用一个 BackBuffer)
下面就是引入 Triple Buffer 之后,解决了 Double Buffer 中遇到的由于 Buffer 不足引起的掉帧问题
这里把两个图放到一起来看,方便大家做对比(一个是 Double Buffer 掉帧两次,一个是使用 Triple Buffer 只掉了一帧)
从上一节 Double Buffer 和 Triple Buffer 的对比图可以看到,在这种情况下(出现连续主线程超时),三个 Buffer 的轮转有助于缓解掉帧出现的次数(从掉帧两次 -> 只掉帧一次)
所以从第一节如何定义掉帧这里我们就知道,App 主线程超时不一定会导致掉帧,由于 Triple Buffer 的存在,部分 App 端的掉帧(主要是由于 GPU 导致),到 SurfaceFlinger 这里未必是掉帧,这是看 Systrace 的时候需要注意的一个点
双 Buffer 的轮转, App 主线程有时候必须要等待 SurfaceFlinger(消费者)释放 Buffer 后,才能获取 Buffer 进行生产,这时候就有个问题,现在大部分手机 SurfaceFlinger 和 App 同时收到 Vsync 信号,如果出现App 主线程等待 SurfaceFlinger(消费者)释放 Buffer ,那么势必会让 App 主线程的执行时间延后,比如下面这张图,可以明显看到:Buffer B 并不是在 Vsync 信号来的时候开始被消费(因为还在使用),而是等 Buffer A 被消费后,Buffer B 被释放,App 才能拿到 Buffer B 进行生产,这期间就有一定的延迟,会让主线程可用的时间变短
我们来看一下在 Systrace 中的上面这种情况发生的时候的表现
而 三个 Buffer 轮转的情况下,则基本不会有这种情况的发生,渲染线程一般在 dequeueBuffer 的时候,都可以顺利拿到可用的 Buffer (当然如果 dequeueBuffer 本身耗时那就不是这里的讨论范围了)
这个比较好理解,双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现 SurfaceFlinger 无法及时合成而导致掉帧
在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间
比如下面两张图,就是对应的 SurfaceFlinger 和 App 的双 Buffer 掉帧情况,由于 SurfaceFlinger 本身就比较耗时(特定场景),而 App 的 dequeueBuffer 得不到及时的响应,导致发生了比较严重的掉帧情况。在换成 Triple Buffer 之后,这种情况就基本上没有了
dumpsys SurfaceFlinger 可以查看 SurfaceFlinger 输出的众多当前的状态,比如一些性能指标、Buffer 状态、图层信息等,后续有篇幅的话可以单独拿出来讲,下面是截取的 Double Buffer 情况下和 Triple Buffer 情况下的各个 App 的 Buffer 使用情况,可以看到不同的 App,在负载不一样的情况下,对 Triple Buffer 的使用率是不一样的;Double Buffer 则完全使用的是双 Buffer
不同 Android 版本属性设置不一样(这是 Google 的一个逻辑 Bug,Android 10 上面已经修复了)
1 | //控制代码 |
修改对应的属性值,然后重启 Framework
1 | //按顺序执行下面的语句(需要 Root 权限) |
1 | //控制代码 |
修改对应的属性值,然后重启 Framework
1 | //按顺序执行下面的语句(需要 Root 权限) |
本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件
一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
Android 的大部分进程间通信都使用 Binder,这里对 Binder 不做过多的解释,想对 Binder 的实现有一个比较深入的了解的话,推荐你阅读下面三篇文章
之所以要单独讲 Systrace 中的 Binder 和锁,是因为很多卡顿问题和响应速度的问题,是因为跨进程 binder 通信的时候,锁竞争导致 binder 通信事件变长,影响了调用端。最常见的就是应用渲染线程 dequeueBuffer 的时候 SurfaceFlinger 主线程阻塞导致 dequeueBuffer 耗时,从而导致应用渲染出现卡顿; 或者 SystemServer 中的 AMS 或者 WMS 持锁方法等待太多, 导致应用调用的时候等待时间比较长导致主线程卡顿
这里放一张文章里面的 Binder 架构图 , 本文主要是以 Systrace 为主,所以会讲 Systrace 中的 Binder 表现,不涉及 Binder 的实现
Binder 主要是用来跨进程进行通信,可以看下面这张图,简单显示了在 Systrace 中 ,Binder 通信是如何显示的
图中主要是 SystemServer 进程和 高通的 perf 进程通信,Systrace 中右上角 ViewOption 里面勾选 Flow Events 就可以看到 Binder 的信息
点击 Binder 可以查看其详细信息,其中有的信息在分析问题的时候可以用到,这里不做过多的描述
对于 Binder,这里主要介绍如何在 Systrace 中查看 Binder 锁信息和锁等待这两个部分,很多卡顿和响应问题的分析,都离不开这两部分信息的解读,不过最后还是要回归代码,找到问题后,要读源码来理顺其代码逻辑,以方便做相应的优化工作
monitor contention with owner Binder:1605_B (4667) at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733) waiters=2 blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)
上面的话分两段来看,以 blocking 为分界线
monitor contention with owner Binder:1605_B (4667) at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733) waiters=2
Monitor 指的是当前锁对象的池,在 Java 中,每个对象都有两个池,锁(monitor)池和等待池:
锁池(同步队列 SynchronizedQueue ):假设线程 A 已经拥有了某个对象(注意:不是类 )的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中。
这里用了争夺(contention)这个词,意思是这里由于在和目前对象的锁正被其他对象(Owner)所持有,所以没法得到该对象的锁的拥有权,所以进入该对象的锁池
Owner : 指的是当前拥有这个对象的锁的对象。这里是 Binder:1605_B,4667 是其线程 ID。
at 后面跟的是拥有这个对象的锁的对象正在做什么。这里是在执行 void com.android.server.wm.ActivityTaskManagerService.activityPaused 这个方法,其代码位置是 :ActivityTaskManagerService.java:1733 其对应的代码如下:
com/android/server/wm/ActivityTaskManagerService.java
1 |
|
可以看到这里 synchronized (mGlobalLock) ,获取了 mGlobalLock 锁的拥有权,在他释放这个对象的锁之前,任何其他的调用 synchronized (mGlobalLock) 的地方都得在锁池中等待
waiters 值得是锁池里面正在等待锁的操作的个数;这里 waiters=2 表示目前锁池里面已经有一个操作在等待这个对象的锁释放了,加上这个的话就是 3 个了
blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)
第二段信息相对来说简单一些,就是标识了当前被阻塞等锁的方法 , 这里是 ActivityManager 的 getFocusedStackInfo 被阻塞,其对应的代码
com/android/server/wm/ActivityTaskManagerService.java
1 |
|
可以看到这里也是调用了 synchronized (ActivityManagerService.this) ,从而需要等待获取 ams 对象的锁拥有权
上面这段话翻译过来就是
ActivityTaskManagerService 的 getFocusedStackInfo 方法在执行过程中被阻塞,原因是因为执行同步方法块的时候,没有拿到同步对象的锁的拥有权;需要等待拥有同步对象的锁拥有权的另外一个方法 ActivityTaskManagerService.activityPaused 执行完成后,才能拿到同步对象的锁的拥有权,然后继续执行
可以对照原文看上面的翻译
monitor contention with owner Binder:1605_B (4667)
at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733)
waiters=2
blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)
还是上面那个 Systrace,Binder 信息里面显示 waiters=2,意味着前面还有两个操作在等锁释放,也就是说总共有三个操作都在等待 Binder:1605_B (4667) 释放锁,我们来看一下 Binder:1605_B 的执行情况
从上图可以看到,Binder:1605_B 正在执行 activityPaused,中间也有一些其他的 Binder 操作,最终 activityPaused 执行完成后,释放锁
下面我们就把这个逻辑里面的执行顺序理顺,包括两个 waiters
上图中可以看到 mGlobalLock 这个对象锁的争夺情况
经过上面四步,就形成了 Binder_1605_B 线程在运行,其他三个争夺 mGlobalLock 对象锁失败的线程分别进入 sleep 状态,等待 Binder_1605_B 执行结束后释放 mGlobalLock 对象锁
上图可以看到 mGlobalLock 锁的释放和后续的流程
经过上面 6 步,这一轮由于 mGlobalLock 对象锁引起的等锁现象结束。这里只是一个简单的例子,在实际情况下,SystemServer 中的 BInder 等锁情况会非常严重,经常 waiter 会到达 7 - 10 个,非常恐怖,比如下面这种:
这也就可以解释为什么 Android 手机 App 安装多了、用的久了之后,系统就会卡的一个原因;另外重启后也会有短暂的时候出现这种情况
如果不知道怎么查看唤醒信息,可以查看: Systrace中查看进程信息唤醒 这篇文章
art/runtime/monitor.cc
1 | std::string Monitor::PrettyContentionInfo(const std::string& owner_name, |
art/runtime/monitor.cc
1 | if (ATRACE_ENABLED()) { |
本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件
一个人可以走的更快 , 一群人可以走的更远
博客的每次更新都会更新这篇目录,方便大家查阅。我会尽量保证每周一更,学无止境,与大家共勉,有什么想了解的或者博客中不足的地方,请大家在博客或者知乎、微博、微信留言给我,我会积极改正。
Systrace 工具是分析 Android 性能问题的利器,它可以从一个图形的角度,来展现整机的运行情况。Systrace 工具不仅可以分析性能问题,用它来进行 Framework 的学习也是很好的,这也是我写本系列文章的一个原因
流畅性主要指的是卡顿、掉帧,对应的英文是 Smooth vs Jank
响应速度主要指的是 App 冷热启动、界面跳转速度、亮灭屏速度等,对应的英文是 Fast vs Slow
主要记录 Android 内存优化相关的知识和工具,以及对系统的影响
博客中 Framework 相关的内容会集中在这里,包括一些 Framework 的运行原理、Framework 问题的解题思路、Framework 优化方法等
这里主要记录一些 App 开发相关的博文,由于写的比较早,大家随便看一下就可以了
与技术无关,但是可以提高幸福感和工作效率
性能优化典范是 Google 出品的一系列性能相关的短视频,总共出了 6 季,之前想的是每一集都来一个文章配合,后面发现不是很现实;Android Tips 则是翻译的另外一个博主的文章
知乎专栏会搬运一部分文章,这里只贴一些高赞的回答
NewsLetter 地址 :https://androidweekly.zhubai.love/ , 推荐使用 邮箱 或者 微信订阅
知乎专栏地址 :Android Weekly , 欢迎大家点赞收藏
这一部分整理之后会放出来,不过大家都知道,演讲或者培训的时候,PPT 里面不会有太多的东西,多数只是一个大纲
]]>本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些
Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC,HWC 可生成 VSYNC 事件并通过回调将事件发送到 SurfaceFlinge , DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号
在 Android 基于 Choreographer 的渲染机制详解 这篇文章里面,我们有提到 :Choreographer 的引入,主要是配合 Vsync,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms,Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用
渲染层(App)与 Vsync 打交道的是 Choreographer,而合成层与 Vsync 打交道的,则是 SurfaceFlinger。SurfaceFlinger 也会在 Vsync 到来的时候,将所有已经准备好的 Surface 进行合成操作
下图显示在 Systrace 中,SurfaceFlinger 进程中的 VSYNC_APP 和 VSYNC_SF 的情况
首先我们要大概了解 Android 中的图形数据流的方向,从下面这张图,结合 Android 的图像流,我们大概把从 App 绘制到屏幕显示,分为下面几个阶段:
下面这张图也是官方的一张图,结合上面的阶段,从左到右看,可以看到一帧的数据是如何在各个进程之间流动的
了解了 Android 中的图形数据流的方向,我们就可以把上面这个比较抽象的数据流图,在 Systrace 上进行映射展示
上图中主要包含 SurfaceFlinger、App 和 hwc 三个进程,下面就来结合图中的标号,来进一步说明数据的流向
文章最开始有提到,Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC,HWC 可生成 VSYNC 事件并通过回调将事件发送到 SurfaceFlinge , DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号.
其中 app 和 sf 相对 hw_vsync_0 都有一个偏移,即 phase-app 和 phase-sf,如下图
Vsync Offset 我们指的是 VSYNC_APP 和 VSYNC_SF 之间有一个 Offset,即上图中 phase-sf - phase-app 的值,这个 Offset 是厂商可以配置的。如果 Offset 不为 0,那么意味着 App 和 SurfaceFlinger 主进程不是同时收到 Vsync 信号,而是间隔 Offset (通常在 0 - 16.6ms 之间)
目前大部分厂商都没有配置这个 Offset,所以 App 和 SurfaceFlinger 是同时收到 Vsync 信号的.
可以通过 Dumpsys SurfaceFlinger 来查看对应的值
Offset 为 0:(sf phase - app phase = 0)
1 | Sync configuration: |
Offset 不为 0 (SF phase - app phase = 4 ms)
1 | Sync configuration: |
下面以 Systrace 为例,来看 Offset 在 Systrace 中的表现
首先说 Offset 为 0 的情况, 此时 App 和 SurfaceFlinger 是同时收到 Vsync 信号 , 其对应的 Systrace 图如下:
这个图上面也有讲解,这里就不再详细说明,大家只需要看到,App 渲染好的 Buffer,要等到下一个 Vsync-SF 来的时候才会被 SurfaceFlinger 拿去做合成,这个时间大概在 16.6 ms。这时候大家可能会想,如果 App 的 Buffer 渲染结束,Swap 到 BufferQueue 中 ,就触发 SurfaceFlinger 去做合成,那岂不是省了一些时间(0-16.6ms )?
答案是可行的,这也就引入了 Offset 机制,在这种情况下,App 先收到 Vsync 信号,进行一帧的渲染工作,然后过了 Offset 时间后,SurfaceFlinger 才收到 Vsync 信号开始合成,这时候如果 App 的 Buffer 已经 Ready 了,那么 SurfaceFlinger 这一次合成就可以包含 App 这一帧,用户也会早一点看到。
下图中,就是一个 Offset 为 4ms 的案例,App 收到 Vsync 4 ms 之后,SurfaceFlinger 才收到 Vsync 信号
Offset 的一个比较难以确定的点就在于 Offset 的时间该如何设置,这也是众多厂商默认都不进行配置 Offset 的一个原因,其优缺点是动态的,与机型的性能和使用场景有很大的关系
这里需要说明的是,不是每次申请 Vsync 都会由硬件产生 Vsync,只有此次请求 vsync 的时间距离上次合成时间大于 500ms,才会通知 hwc,请求 HW_VSYNC
以桌面滑动为例,看 SurfaceFlinger 的进程 Trace 可以看到 HW_VSYNC 的状态
后续 App 申请 Vsync 时候,会有两种情况,一种是有 HW_VSYNC 的情况,一种是没有有 HW_VSYNC 的情况
HW_VSYNC 主要是利用最近的硬件 VSYNC 来做预测,最少要 3 个,最多是 32 个,实际上要用几个则不一定, DispSync 拿到 6 个 VSYNC 后就会计算出 SW_VSYNC,只要收到的 Present Fence 没有超过误差,硬件 VSYNC 就会关掉,不然会继续接收硬件 VSYNC 计算 SW_VSYNC 的值,直到误差小于 threshold.关于这一块的计算具体过程,可以参考这篇文章: S W-VS YN C 的生成与传递 ,关于这一块的流程大家也可以参考这篇文章,里面有更细节的内容,这里摘录了他的结论
SurfaceFlinger 通过实现了 HWC2::ComposerCallback 接口,当 HW-VSYNC 到来的时候,SurfaceFlinger 将会收到回调并且发给 DispSync。DispSync 将会把这些 HW-VSYNC 的时间戳记录下来,当累计了足够的 HW-VSYNC 以后(目前是大于等于 6 个),就开始计算 SW-VSYNC 的偏移 mPeriod。计算出来的 mPeriod 将会用于 DispSyncThread 用来模拟 HW-VSYNC 的周期性起来并且通知对 VSYNC 感兴趣的 Listener,这些 Listener 包括 SurfaceFlinger 和所有需要渲染画面的 app。这些 Listener 通过 EventThread 以 Connection 的抽象形式注册到 EventThread。DispSyncThread 与 EventThread 通过 DispSyncSource 作为中间人进行连接。EventThread 在收到 SW-VSYNC 以后将会把通知所有感兴趣的 Connection,然后 SurfaceFlinger 开始合成,app 开始画帧。在收到足够多的 HW-VSYNC 并且在误差允许的范围内,将会关闭通过 EventControlThread 关闭 HW-VSYNC。
待更新
一个人可以走的更快 , 一群人可以走的更远
另外我还加上了部分系统厂商所做的启动相关的优化,不过只写了一些我知道的,还有一些厂商有黑科技,就不在这里的讨论范围了。知道厂商做的事情,可能也会帮助到你,比如联系厂商做白名单、接入厂商 SDK 等
应用的启动,从桌面点击应用图标到主界面用户可操作,大致遵循下面的流程:
可以看到应用启动过程中,最重要的两个进程就是 SystemServer 和 App Process . 其职责划分如下:
这里还需要引入冷启动和热启动的概念,这也是我们经常会碰到的两个概念
各家应该都有自己的方案,关键在于如何定义启动结束的点,这个也是一直困扰我的一个地方,有的应用很好定义,有的应用则因为比较复杂,无法直接衡量启动速度。像 adb 这种方法自己玩玩可以,生产环境没啥用;录屏本身就有性能损耗..
这里我建议大家学习历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)中提到的测量方法:自动化、稳定、持续集成
通过OCR提取图片中的文字信息作为关键特征。该算法的优势:1. 在于应用页面上基本都是有文字的, OCR也可以识别到图片上的文字, 文字出现则图片加载完成, 和用户体感是一致的;2. 文字作为特征,过滤掉了很多图片特征可能带来的噪声, 减少了算法调试的工作量;另外阿里集团内有非常成熟和优秀的OCR服务——读光,文档识别率超过99.7%, 使用水滴平台封装的OCR服务,可以快速接入和使用。最终的识别方案就是基于OCR识别来进行的
启动窗口,也叫启动页、SplashWindow、StartingWindow 等,指的是应用启动时候的预览窗口。iOS App 强制有一个启动页,用户点击桌面 App 图标之后,系统会立即显示这个启动窗口,等 App 主页加载好之后再显示主页面。Android 也有类似的机制 (启动窗口这个是 Android 系统提供的),但是也提供了一个接口,让应用开发者设置是否显示这个启动窗口(默认是显示),部分开发者会把这个系统提供的启动窗口禁掉,启动自己的窗口。
但是启动自己的窗口需要的时间要比直接显示系统的启动窗口所花的时间要长,这就会导致用户在使用的时候,点击图标启动 App 的时候,有一定的延迟,表现在点击图标过了一段时间才进行窗口动画进入 App,我们要尽量避免这种情况
线程优化主要是减少 CPU 调度带来的波动,让启动时间更稳定。如果启动过程中有太多的线程一起启动,会给 CPU 带来非常大的压力,尤其是比较低端的机器。过多的线程同时跑会让主线程的 Sleep 和 Runnable 状态变多, 增加了应用的启动速度,优化的过程中要注意:
应用启动的时候,如果主线程的工作过多,也会造成主线程过于繁忙,下面几个系统调度相关的点需要注意:
启动过程中繁忙的 cpu
启动过程中繁忙的 SystemServer
启动过程中减少 GC 的次数
可以参考下面这篇文章 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」)
启动过程中负载比较高,有许多系统 IO 都在此时发生,这时候 IO 的性能下降会比较快,此时 App 中的 IO 操作会比平时更慢一些,尤其是在性能比较差的机器上。
IO 分网络 IO 和磁盘 IO ,启动过程中不建议进行网络 IO ,对于磁盘 IO 则要细扣,邵文在高手课里面有讲到:
下面图中可以看到低内存的时候,启动应用主线程有较多的 IO 等待(UI Thread 这一栏,橘红色代表 IO 等待 )
利用 Linux 的 IO 读取策略,PageCache 和 ReadAhead 机制,按照读取顺序重新排列,减少磁盘 IO 次数 。具体操作可以参考支付宝 App 构建优化解析:通过安装包重排布优化 Android 端启动性能 这篇文章
Linux 底层文件系统中 VFS 上次 App 进程之间,存在一层 pagecache,pagecache 由内存中的物理 page 组成,其内容对应磁盘上的 block。Pagecache 的大小是动态变化的,可以扩大,也可以在内存不足时缩小。Cache 缓存的存储设备被称为后备存储(backing store),一个 page 通常包含多个 block,这些 block 不一定是连续的
利用文件重布局结合Pagecache 机制可以减少启动过程中的真正 IO 的次数,简单的说,通过文件重布局的目的,就是将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的
类重排的实现通过 ReDex 的 Interdex 调整类在 Dex 中的排列顺序。Interdex 优化不需要去分析类引用,它只需要调整 Dex 中类的顺序,把启动时需要加载的类按顺序放到主 dex 里,这个工作我们完全可以在编译过程中实现,而且这个优化可以提升启动速度,优化效果从 facebook 公布的数据来看也比较可观,性价比高。具体实现可以参考 Redex 初探与 Interdex:Andorid 冷启动优化
应用主界面布局优化是老生常谈了,综合起来无非就是下面两点,这个需要结合具体的界面布局去做优化,网上也有比较多的资料可以查阅
IdleHandler:当 Handler 空闲的时候才会被调用,如果返回 true, 则会一直执行,如果返回 false,执行完一次后就会被移除消息队列。比如,我们可以将从服务器获取推送 Token 的任务放在延迟 IdleHandler 中执行,或者把一些不重要的 View 的加载放到 IdleHandler 中执行
可以在 systrace 生成的文件中看到 verifyClass 过程,因为需要校验方法的每一个指令,所以是一个比较耗时的操作。
App 瘦身包括代码瘦身和资源瘦身,通常的做法如下:
启动优化整个流程的梳理,流程的梳理,我们这里引入了一个有向无环图的概念,我们会把整个的概念梳理成有向无环图的结构,然后会去挨个加载。右边的部分,可以看到我们其实在启动的时候,首先会去加载一些必要的启动项,必要的启动项是左边流程,会用一个多进程的方式加载,以来有向无环图进行控制,比如说我是在非必须的时候启动加载我可以放在后面再去加载。当然在整个有向无环图的顺序加载,其实还是会做一些进程的判断,要判断某些项目是不是要在主进程里加载,某些要在初始进程里面加载
从 Spark 的 DAGScheduler 中领悟到它的核心思想,面向阶段调度(Stage-Oriented Scheduler):把应用划分成一个个的阶段(Stage),再把任务(Task)安排到各个阶段中去,任务的编排则是通过构建 有向无环图(DAG),把任务依赖通过图的方式梳理得 井井有条。因为它分阶段执行,先集中资源把阶段一搞定,再齐心协力去执行阶段二,这样即能控制拥塞,又能保证时序,还能并发执行,让设备性能尽可能得到发挥
大家可以参考淘宝的全链路优化的案例:历时1年,上百万行代码!首次揭秘手淘全链路性能优化(上)
Activity 打开之前就预加载数据,在 Activity 的 UI 布局初始化完成后显示预加载的数据,大大缩短启动时间。 可以参考 :https://github.com/luckybilly/PreLoader/blob/master/README-zh-CN.md
保活,是各个应用开发者的噩梦,也是 Android 厂商关注和打击的重点。不过从启动的角度来看,如果应用进程不被杀,那么启动自然就快了,所以保活对应用启动速度也是有极大的帮助。
当然这里说的保活,并不是建议大家用各种黑科技、相互唤醒、通知轰炸这种保活手段,而是提供真正的功能,能让用户觉得你在后台是合理的、可以接收的。比如在后台的时候,资源能释放的都释放掉,不要一直在后台做耗电操作,该停的服务停掉,该关的动画关掉。
当然对于应用开发者来说,上面说的都太多理想化了,而且目前的手机厂商也会很暴力,应用到了后台就会处理掉,不过这毕竟是一个方向,Google 也在规范应用后台行为和规范厂商处理应用这两方面都在做努力,Android 系统的生态,还是需要应用开发者和 Android 厂商一起取改善。
当然保活还有一条路就是走跟厂商的合作,优化后台内存、去掉重复拉起、去掉流氓逻辑、积极响应低内存警告,做好这些话后可以跟系统厂商联系,谈放到查杀白名单和自启动白名单的可行性
这里涉及到具体的业务,每个 App 都不一样,但是所要做的事情都是一样的,下面是邵文在高手课里面提到的:
可以把具体的业务分为下面四个维度(此处图文来自https://juejin.im/post/5c21ea325188254eaa5c45b1#heading-5)
然后按需进行加载优化
具体的业务会有具体的优化场景,大家可以参考这篇文章中的优化流程和优化项https://www.jianshu.com/p/f5514b1a826c
- 数据库及IO操作都移到工作线程,并且设置线程优先级为THREAD_PRIORITY_BACKGROUND,这样工作线程最多能获取到10%的时间片,优先保证主线程执行
- 流程梳理,延后执行;实际上,这一步对项目启动加速最有效果。通过流程梳理发现部分流程调用时机偏失等, 例如
- 更新等操作无需在首屏尚未展示就调用,造成资源竞争
- 调用了IOS为了规避审核而做的开关,造成网络请求密集
- 自有统计在Application的调用里创建数量固定为5的线程池,造成资源竞争
- 修改广告闪屏逻辑为下次生效
- 去掉用无但被执行的老代码
- 去掉开发阶段使用但线上被执行的代码
- 去掉重复逻辑执行代码
- 去掉调用三方SDK里或者Demo里的多余代码
- 信息缓存,常用信息只在第一次获取,之后从缓存中取
- 项目是多进程架构,只在主进程执行Application的onCreate()
StartingWindow 会在用户点击 App 后立即创建并显示(前提是 App 没有禁止 StartingWindow),在 AppWindow 创建好之后,StartingWindow 消失,AppWindow 显示
1 | StartingWindow(SystemWindow) |
1 | StartingWindow(SystemWindow) |
1 | StartingWindow(SystemWindow) |
1 | SplashActivity(AppWindow) |
其实对用户来说,第一种启动流程是最好的,只涉及到一次窗口的切换;但是部分 App 由于广告页的需求,会使用第二种流程 ;但是尽量不要使用第三种和第四种启动流程,体验非常不好
除了 App 自身的优化之外,Android 框架对应用启动也是非常关注的,做了比较多的优化,下面简单说一下思路,各个厂商的实现也不太一样,但是基本上都会有,有些是硬核代码优化,有的是利用系统策略做优化。
厂商的策略各不相同,这里只是简单的提一下思路
App 启动的时候,系统会对要启动的应用做绝对的资源倾斜,比如 CPU、IO、GPU 等,这一点大家抓个 Systrace 看一下即可,不管是频率还是调度算法,正在启动的 App 绝对是当时的系统 VIP 客户
部分厂商也提供了资源调度的 SDK ,应用可以接入这些 SDK,在需要资源的时候直接调用 SDK 获取
Android Q 加入了 PreFork 机制,会先 fork 几个空进程,当 App 启动的时候,可以直接复用这几个空进程,而不用重新去 fork
1 | 2,348K: usap32 (pid 18731) |
启动的时候,对启动过程中的 Message 进行重新排列
部分厂家会对启动过程 App 的主线程和渲染线程做特殊对待,比如让他们直接跑到大核上,将其他不重要的线程移到小核
部分场景会针对用户的使用习惯进行学习,比如在什么时间、什么场合、什么交通工具打开手机,系统会预测你要启动的 App,并在后台进行启动,这样你点击这个 App 的时候,就已经是热启动了
系统也会对一些应用进行特殊处理,以提升用户体验:包括但不限于 进程\线程优先级调整、查杀白名单、用户常用应用记录等,进行适当的后台保活,下次启动的时候就是热启动了
系统会对一些应用进行特殊处理,比如这个 App 比较重要但是不能杀掉,那么有的厂商会在这种应用退到后台之后,进行无感重启:比如说某个应用内存超标或者持续 Crash ,后台重启可以很好地解决这个问题,这样重启后的 App 是用户点击启动的时候就是热启动
部分应用启动的时候,需要大量的内存,比如现在的相机启动,这时候如果没有足够的内存,那么系统必须要通过杀掉很多应用、释放 Cache 等操作来给这个 App 让路,这个过程会使得这些大内存的 App 在启动的时候频繁进行内存操作,导致启动速度变慢
部分厂商会在监测到这种大内存 App 启动的时候,提前做内存的回收操作,这样在启动的时候,就有了足够的内存给这个 App 使用
Android 系统更新也会对应用启动速度进行优化,比如上面提到的 Pre-Fork,又比如这里的简化 doFrame 个数
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些
这里以滑动列表为例 ,我们截取主线程和渲染线程一帧的工作流程(每一帧都会遵循这个流程,不过有的帧需要处理的事情多,有的帧需要处理的事情少) ,重点看 “UI Thread ” 和 RenderThread 这两行
这张图对应的工作流程如下
上面这个流程在 Android 基于 Choreographer 的渲染机制详解 这篇文章里面已经介绍的很详细了,包括每一帧的 doFrame 都在做什么、卡顿计算的原理、APM 相关. 没有看过这篇文章的同学,建议先去扫一眼
那么这篇文章我们主要从 Android 基于 Choreographer 的渲染机制详解 这篇文章没有讲到的几个点来入手,帮你更好地理解主线程和渲染线程
Android App 的进程是基于 Linux 的,其管理也是基于 Linux 的进程管理机制,所以其创建也是调用了 fork 函数
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
1 | pid_t pid = fork(); |
Fork 出来的进程,我们这里可以把他看做主线程,但是这个线程还没有和 Android 进行连接,所以无法处理 Android App 的 Message ;由于 Android App 线程运行基于消息机制 ,那么这个 Fork 出来的主线程需要和 Android 的 Message 消息绑定,才能处理 Android App 的各种 Message
这里就引入了 ActivityThread ,确切的说,ActivityThread 应该起名叫 ProcessThread 更贴切一些。ActivityThread 连接了 Fork 出来的进程和 App 的 Message ,他们的通力配合组成了我们熟知的 Android App 主线程。所以说 ActivityThread 其实并不是一个 Thread,而是他初始化了 Message 机制所需要的 MessageQueue、Looper、Handler ,而且其 Handler 负责处理大部分 Message 消息,所以我们习惯上觉得 ActivityThread 是主线程,其实他只是主线程的一个逻辑处理单元。
App 进程 fork 出来之后,回到 App 进程,查找 ActivityThread 的 Main函数
com/android/internal/os/ZygoteInit.java
1 | static final Runnable childZygoteInit( |
这里的 startClass 就是 ActivityThread,找到之后调用,逻辑就到了 ActivityThread的main函数
android/app/ActivityThread.java
1 | public static void main(String[] args) { |
注释里面都很清楚,这里就不详细说了,main 函数处理完成之后,主线程就算是正式上线开始工作,其 Systrace 流程如下:
另外我们经常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了
1 | class H extends Handler { //摘抄了部分 |
可以看到,进程创建、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,然后进到具体的 handleXXX
主线程讲完了我们来讲渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工作都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分之前主线程的渲染工作,减轻主线程的负担
我们一般提到的硬件加速,指的就是 GPU 加速,这里可以理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个
1 | android:hardwareAccelerated="false" |
我们就可以关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,直接 cpu 调用 libSkia 来进行渲染。其 Systrace 的表现如下
与这篇文章开头的开了硬件加速的那个图对比,可以看到主线程由于要进行渲染工作,所以执行的时间变长了,也更容易出现卡顿,同时帧与帧直接的空闲间隔也变短了,使得其他 Message 的执行时间被压缩
正常情况下,硬件加速是开启的,主线程的 draw 函数并没有真正的执行 drawCall ,而是把要 draw 的内容记录到 DIsplayList 里面,同步到 RenderThread 中,一旦同步完成,主线程就可以被释放出来做其他的事情,RenderThread 则继续进行渲染工作
渲染线程初始化在真正需要 draw 内容的时候,一般我们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,如果没有则去进行初始化
android/view/ViewRootImpl.java
1 | mAttachInfo.mThreadedRenderer.initializeIfNeeded( |
后续直接调用 draw
android/graphics/HardwareRenderer.java
1 | mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this); |
上面的 draw 只是更新 DIsplayList ,更新结束后,调用 syncAndDrawFrame ,通知渲染线程开始工作,主线程释放。渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui
frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
1 | int RenderProxy::syncAndDrawFrame() { |
关于 RenderThread 的工作流程这里就不细说了,后续会有专门的篇幅来讲解这个,目前 hwui 这一块的流程也有很多优秀的文章,大家可以对照文章和源码来看,其核心流程在 Systrace 上的表现如下:
主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DIsplayList ,但是不涉及 SurfaceFlinger 打交道;渲染线程负责渲染渲染相关的工作,一部分工作也是 CPU 来完成的,一部分操作是调用 OpenGL 函数来完成的
当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类,这样间接的进行绘制操作的优点如下
RenderThread 的具体流程大家可以看这篇文章 : http://www.cocoachina.com/articles/35302
游戏大多使用单独的渲染线程,有单独的 Surface ,直接跟 SurfaceFlinger 进行交互,其主线程的存在感比较低,绝大部分的逻辑都是自己在自己的渲染线程里面实现的。
大家可以看一下王者荣耀对应的 Systrace ,重点看应用进程和 SurfaceFlinger 进程(30fps)
可以看到王者荣耀主线程的主要工作,就是把 Input 事件传给 Unity 的渲染线程,渲染线程收到 Input 事件之后,进行逻辑处理,画面更新等。
这里提一下 Flutter App 在 Systrace 上的表现,由于 Flutter 的渲染是基于 libSkia 的,所以它也没有 RenderThread ,而是他自建的 RenderEngine , Flutter 比较重要的两个线程是 ui 线程和 gpu 线程,对应到下面提到的 Framework 和 Engine 两层
Flutter 中也会监听 Vsync 信号 ,其 VsyncView 中会以 postFrameCallback 的形式,监听 doFrame 回调,然后调用 nativeOnVsync ,将 Vsync 到来的信息传给 Flutter UI 线程,开始一帧的绘制。
可以看到 Flutter 的思路跟游戏开发的思路差不多,不依赖具体的平台,自建渲染管道,更新快,跨平台优势明显。
Flutter SDK 自带 Skia 库,不用等系统升级就可以用到最新的 Skia 库,而且 Google 团队在 Skia 上做了很多优化,所以官方号称性能可以媲美原生应用
Flutter 的框架分为 Framework 和 Engine 两层,应用是基于 Framework 层开发的,Framework 负责渲染中的 Build,Layout,Paint,生成 Layer 等环节。Engine 层是 C++实现的渲染引擎,负责把 Framework 生成的 Layer 组合,生成纹理,然后通过 Open GL 接口向 GPU 提交渲染数据。
当需要更新 UI 的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework,然后 Framework 会进行 animations, build,layout,compositing,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后通过 Open Gl 接口提交数据给 GPU,GPU 经过处理后在显示器上面显示。整个流程如下图:
如果主线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。主线程执行超时通常会带来两个问题
对于用户来说,这两个情况都是用户不愿意看到的,所以对于 App 开发者来说,两个问题是发版本之前必须要解决的,ANR 这个由于有详细的调用栈,所以相对来说比较好定位;但是间歇性卡顿这个,可能就需要使用工具来进行分析了:Systrace + TraceView,所以理解主线程和渲染线程的关系和他们的工作原理是非常重要的,这也是本系列的一个初衷
另外关于卡顿,可以参考下面三篇文章,你的 App 卡顿不一定是你 App 的问题,也有可能是系统的问题,不过不管怎么说,首先要会分析卡顿问题。
本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - MainThread 和 RenderThread 解读
一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
在Android 基于 Choreographer 的渲染机制详解 这篇文章中,我有讲到,Android App 的主线程运行的本质是靠 Message 驱动的,这个 Message 可以是循环动画、可以是定时任务、可以是其他线程唤醒,不过我们最常见的还是 Input Message ,这里的 Input 是以 InputReader 这里的分类,不仅包含触摸事件(Down、Up、Move) , 可包含 Key 事件(Home Key 、 Back Key) . 这里我们着重讲的是触摸事件
由于 Android 系统在 Input 链上加了一些 Trace 点,且这些 Trace 点也比较完善,部分厂家可能会自己加一些,不过我们这里以标准的 Trace 点来讲解,这样不至于你换了个手机抓的 Trace 就不一样了
Input 在 Android 中的地位是很高的,我们在玩手机的时候,大部分应用的滑动、跳转这些都依靠 Input 事件来驱动,后续我会专门写一篇文章,来介绍 Android 中基于 Input 的运行机制。这里是从 Systrace 的角度来看 Input 。看下面的流程之前,脑子里先有个关于 Input 的大概处理流程,这样看的时候,就可以代入:
另外在看 Systrace 的时候,要牢记 Systrace 中时间是从左到右流逝的,也就是说如果你在 Systrace 上画一条竖直线,那么竖直线左边的事件永远比右边的事件先发生,这也是我们分析源码流程的一个基石。我希望大家在看基于 Systrace 的源码流程分析之后,脑子里有一个图形化的、立体的流程图,你跟的代码走到哪一步了在图形你在脑中可以快速定位出来
下面这张图是一个概览图,以滑动桌面为例 (滑动桌面包括一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件,这些事件和事件流都会在 Systrace 上有所体现,这也是我们分析 Systrace 的一个重要的切入点),主要牵扯到的模块是 SystemServer 和 App 模块,其中用蓝色标识的是事件的流动信息,红色的是辅助信息。
InputReader 和 InputDispatcher 是跑在 SystemServer 里面的两个 Native 线程,负责读取和分发 Input 事件,我们分析 Systrace 的 Input 事件流,首先是找到这里。下面针对上图中标号进行简单说明
下面以第一个 Input_Down 事件的处理流程来进行详细的工作流说明,其他的 Move 事件和 Up 事件的处理是一样的(部分不一样,不过影响不大)
放大 SystemServer 的部分,可以看到其工作流(蓝色),滑动桌面包括 Input_Down + 若干个 Input_Move + Input_Up ,我们这里看的是 Input_Down 这个事件
应用在收到 Input 事件后,有时候会马上去处理 (没有 Vsync 的情况下),有时候会等 Vsync 信号来了之后去处理,这里 Input_Down 事件就是直接去唤醒主线程做处理,其 Systrace 比较简单,最上面有个 Input 事件队列,主线程则是简单的处理
主线程处理 Input 事件这个大家比较熟悉,从下面的调用栈可以看到,Input 事件传到了 ViewRootImpl,最终到了 DecorView ,然后就是大家熟悉的 Input 事件分发机制
从上面的 Systrace 来看,Input 事件的基本流向如下:
通过上面的流程,一次 Input 事件就被消耗掉了(当然这只是正常情况,还有很多异常情况、细节处理,这里就不细说了,自己看相关流程的时候可以深挖一下) , 那么本节就从上面的关键流中取几个重要的知识点讲解(部分流程和图参考和拷贝了 Gityuan 的博客的图,链接在最下面参考那一节)
InputReader 是一个 Native 线程,跑在 SystemServer 进程里面,其核心功能是从 EventHub 读取事件、进行加工、将加工好的事件发送到 InputDispatcher
InputReader Loop 流程如下
核心代码 loopOnce 处理流程如下:
InputReader 核心 Loop 函数 loopOnce 逻辑如下
1 | void InputReader::loopOnce() { |
上面的 InputReader 调用 mQueuedListener->flush 之后 ,将 Input 事件加入到InputDispatcher 的 mInboundQueue ,然后唤醒 InputDispatcher , 从 Systrace 的唤醒信息那里也可以看到 InputDispatch 线程是被 InputReader 唤醒的
InputDispatcher 的核心逻辑如下:
其核心处理逻辑在 dispatchOnceInnerLocked 这里
1 | void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) { |
InputDispatcher 执行 notifyKey 的时候,会将 Input 事件封装后放到 InboundQueue 中,后续 InputDispatcher 循环处理 Input 事件的时候,就是从 InboundQueue 取出事件然后做处理
Outbound 意思是出站,这里的 OutboundQueue 指的是要被 App 拿去处理的事件队列,每一个 App(Connection) 都对应有一个 OutboundQueue ,从 InboundQueue 那一节的图来看,事件会先进入 InboundQueue ,然后被 InputDIspatcher 派发到各个 App 的 OutboundQueue
当 InputDispatcher 将 Input 事件分发出去之后,将 DispatchEntry 从 outboundQueue 中取出来放到 WaitQueue 中,当 publish 出去的事件被处理完成(finished),InputManagerService 就会从应用中得到一个回复,此时就会取出 WaitQueue 中的事件,从 Systrace 中看就是对应 App 的 WaitQueue 减少
如果主线程发生卡顿,那么 Input 事件没有及时被消耗,也会在 WaitQueue 这里体现出来,如下图:
图来自 Gityuan 博客
Input 的刷新取决于触摸屏的采样,目前比较多的屏幕采样率是 120Hz 和 160Hz ,对应就是 8ms 采样一次或者 6.25ms 采样一次,我们来看一下其在 Systrace 上的展示
可以看到上图中, InputReader 每隔 6.25ms 就可以读上来一个数据,交给 InputDispatcher 去分发给 App ,那么是不是屏幕采样率越高越好呢?也不一定,比如上面那张图,虽然 InputReader 每隔 6.25ms 就可以读上来一个数据给 InputDispatcher 去分发给 App ,但是从 WaitQueue 的表现来看,应用并没有消耗这个 Input 事件,这是为什么呢?
原因在于应用消耗 Input 事件的时机是 Vsync 信号来了之后,刷新率为 60Hz 的屏幕,一般系统也是 60 fps ,也就是说两个 Vsync 的间隔在 16.6ms ,这期间如果有两个或者三个 Input 事件,那么必然有一个或者两个要被抛弃掉,只拿最新的那个。也就是说:
Dumpsys Input 主要是 Debug 用,我们也可以来看一下其中的一些关键信息,到时候遇到了问题也可以从这里面找 , 其命令如下:
1 | adb shell dumpsys input |
其中的输出比较多,我们终点截取 Device 信息、InputReader、InputDispatcher 三段来看就可以了
主要是目前连接上的 Device 信息,下面摘取的是 touch 相关的
1 | 3: main_touch |
InputReader 这里就是当前 Input 事件的一些展示
1 | Device 3: main_touch |
InputDispatch 这里的重要信息主要包括
1 | Input Dispatcher State: |
本文部分图文参考和拷贝自下面几篇文章,同时下面几篇文章讲解了 Input 流程的细节部分,推荐大家在看完这篇文章后,如果对代码细节感兴趣,可以仔细研读下面这几篇非常棒的文章。
本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - Input 解读
一个人可以走的更快 , 一群人可以走的更远
一般的开发者很难发现这个问题,但是如果你经常使用 Systrace ,多开几十个应用然后退回到桌面,左右滑动抓取 Systrace ,就可以很容易发现,总有那么几个后台的应用,还在频繁地做无效的动画。
这里说的后台做动画,指的是由于某种原因,应用在退到后台之后,用户看不到任何这个 App 界面的时候,他仍然在后台不断地更新,耗费 CPU。引起这个问题的原因可能有好多个,毕竟 往 Choreographer 扔 CALLBACK_ANIMATION 的地方太多了,而且每个应用可能都不一样,但最终还是需要各个应用去做修复
下面我们就以两个实例,从技术的角度来看一下事件发生时候的情况和原因,希望看到这篇文章的开发者,检查一下自己的应用是否有这个问题,有则改之,无则恭喜
我们在使用网易新闻后,将网易新闻退到后台,然后左右滑动桌面,抓 Systrace 来看:
网易新闻到后台之后还在持续做 Animation 的回调(红框内),每一帧都还是在 doFrame 操作
放大每一个 doFrame 来看,Choreographer 中的 input 和 traversal 都没有触发,只有 animation 的回调一直在执行
我们把这份 Trace 上的 cpu 部分全选,然后下面按照 Wall Duration 排序,可以发现网易新闻后台动画执行时间最长。应用已经在后台且不可见的时候,还在这么频繁地工作,占用 CPU 资源,消耗电量,实在是不应该
抓对应的 MethodTrace 来看,就是在做动画,没有进行关闭 ,动画依旧在每一帧进行 onAnimationUpdate 的回调 ,可以看到这里是因为使用了 Airbnb 的 Lottie 库导致的,动画没有关闭,所以还是一直在做触发
启动 QQ 音乐,然后回到桌面, 左右滑动桌面并抓取 Systrace 和 MethodTrace ,可以看到跟上面的网易新闻的表现一致
抓取了 QQ 音乐的后台动画时候的 MethodTrace 发现,也是由于退到后台之后,没有暂停动画导致的,也是 Airbnb 的 Lottie 的锅, 而且 QQ 音乐有三个动画没有停止,比网易新闻还要严重一些
放大后可以看到
当然也不是每一个都是 Airbnb 的 Lottie 动画库引起的,比如下面这个,就是普通的动画没有结束
根本原因是应用在不可见之后,没有将动画暂停,导致应用切换到后台之后,依然在刷新动画的回调,但此时由于是不可见的,不会触发 Input Callback 和 draw Callback ,所以也不会有任何的绘制操作,也就是说这个 Animation 的刷新完全是没有意义的(当然也有可能是业务需求?)
上面两个例子里面,网易新闻和 QQ 音乐都是因为使用了 Lottie 来实现动画,但是没有正确的关闭导致的。
Lottie 库的 issue 列表里面有人提到了这个情况:
提出问题:
I recently did some benchmarking on an app which uses lottie to do some animations (autoplay and looping). I noticed that there is quite some CPU usage when the app is in the background and tried to investigate.
It seems to me looping animations do not pause/stop when the containing LottieAnimationView is off screen, and/or the Activity is paused.
I believe this is due to the cleanup code being only in onDetachedFromWindow() which is not necessarily being called once the Activity goes into a paused state and most definitely not, when the view is simply not visible (GONE, INVISIBLE ) anymore.
解决方法:
Overriding LottieAnimationView and doing the following solves the visibility issue for me and Lottie is paused when not visible.
1 |
|
总之就是 : 当 App 不可见的时候,停止所有的动画:pauseAnimation!!!
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
知乎 - Android 中的“后台无效动画“行为分析
掘金 - Android 中的“后台无效动画“行为分析
]]>一个人可以走的更快 , 一群人可以走的更远
Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms , Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用. 了解 Choreographer 还可以帮助 App 开发者知道程序每一帧运行的基本原理,也可以加深对 Message、Handler、Looper、MessageQueue、Measure、Layout、Draw 的理解
本文是 Systrace 系列文章的第八篇,主要是对 Systrace 中的 Choreographer 进行简单介绍
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
在讲 Choreographer 之前,我们先理一下 Android 主线程运行的本质,其实就是 Message 的处理过程,我们的各种操作,包括每一帧的渲染操作 ,都是通过 Message 的形式发给主线程的 MessageQueue ,MessageQueue 处理完消息继续等下一个消息,如下图所示
MethodTrace 图示
Systrace 图示
引入 Vsync 之前的 Android 版本,渲染一帧相关的 Message ,中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,不稳定,如下图
MethodTrace 图示
Systrace 图示
可以看到这时候的瓶颈是在 dequeueBuffer, 因为屏幕是有刷新周期的, FB 消耗 Front Buffer 的速度是一定的, 所以 SF 消耗 App Buffer 的速度也是一定的, 所以 App 会卡在 dequeueBuffer 这里,这就会导致 App Buffer 获取不稳定, 很容易就会出现卡顿掉帧的情况.
对于用户来说,稳定的帧率才是好的体验,比如你玩王者荣耀,相比 fps 在 60 和 40 之间频繁变化,用户感觉更好的是稳定在 50 fps 的情况.
所以 Android 的演进中,引入了 Vsync + TripleBuffer + Choreographer 的机制,其主要目的就是提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。
Choreographer 的引入,主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 至于为什么 Vsync 周期选择是 16.6ms (60 fps) ,是因为目前大部分手机的屏幕都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每隔 16.6 ms ,Vsync 信号到来唤醒 Choreographer 来做 App 的绘制操作 ,如果每个 Vsync 周期应用都能渲染完成,那么应用的 fps 就是 60 ,给用户的感觉就是非常流畅,这就是引入 Choreographer 的主要作用
当然目前使用 90Hz 刷新率屏幕的手机越来越多,Vsync 周期从 16.6ms 到了 11.1ms,上图中的操作要在更短的时间内完成,对性能的要求也越来越高,具体可以看新的流畅体验,90Hz 漫谈 这篇文章
Choreographer 扮演 Android 渲染链路中承上启下的角色
从上面可以看出来, Choreographer 担任的是一个工具人的角色,他之所以重要,是因为通过 Choreographer + SurfaceFlinger + Vsync + TripleBuffer 这一套从上到下的机制,保证了 Android App 可以以一个稳定的帧率运行(20fps、90fps 或者 60fps),减少帧率波动带来的不适感。
了解 Choreographer 还可以帮助 App 开发者知道程序每一帧运行的基本原理,也可以加深对 Message、Handler、Looper、MessageQueue、Input、Animation、Measure、Layout、Draw 的理解 , 很多 APM 工具也用到了 Choreographer( 利用 FrameCallback + FrameInfo ) + MessageQueue ( 利用 IdleHandler ) + Looper ( 设置自定义 MessageLogging) 这些组合拳,深入了解了这些之后,再去做优化,脑子里的思路会更清晰。
另外虽然画图是一个比较好的解释流程的好路子,但是我个人不是很喜欢画图,因为平时 Systrace 和 MethodTrace 用的比较多,Systrace 是按从左到右展示整个系统的运行情况的一个工具(包括 cpu、SurfaceFlinger、SystemServer、App 等关键进程),使用 Systrace 和 MethodTrace 也可以很方便地展示关键流程。当你对系统代码比较熟悉的时候,看 Systrace 就可以和手机运行的实际情况对应起来。所以下面的文章除了一些网图之外,其他的我会多以 Systrace 来展示。
下图以滑动桌面为例子,我们先看一下从左到右滑动桌面的一个完整的预览图(App 进程),可以看到 Systrace 中从左到右,每一个绿色的帧都表示一帧,表示最终我们可以手机上看到的画面
有了上面这个整体的概念,我们将 UI Thread 的每一帧放大来看,看看 Choreogrepher 的位置以及 Choreogrepher 是怎么组织每一帧的
第一步初始化完成后,后续就会在步骤 2-9 之间循环
同时也附上这一帧所对应的 MethodTrace(这里预览一下即可,下面会有详细的大图)
下面我们就从源码的角度,来看一下具体的实现
下面从源码的角度来简单看一下,源码只摘抄了部分重要的逻辑,其他的逻辑则被剔除,另外 Native 部分与 SurfaceFlinger 交互的部分也没有列入,不是本文的重点,有兴趣的可以自己去跟一下。
1 | // Thread local storage for the choreographer. |
1 | private Choreographer(Looper looper, int vsyncSource) { |
1 | private final class FrameHandler extends Handler { |
在 Activity 启动过程,执行完 onResume 后,会调用 Activity.makeVisible(),然后再调用到 addView(), 层层调用会进入如下方法
1 | ActivityThread.handleResumeActivity(IBinder, boolean, boolean, String) (android.app) |
Vsync 的注册、申请、接收都是通过 FrameDisplayEventReceiver 这个类,所以可以先简单介绍一下。 FrameDisplayEventReceiver 继承 DisplayEventReceiver , 有三个比较重要的方法
1 | private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable { |
从下面的函数调用栈可以看到,Choreographer 的内部类 FrameDisplayEventReceiver.onVsync 负责接收 Vsync 回调,通知 UIThread 进行数据处理。
那么 FrameDisplayEventReceiver 是通过什么方式在 Vsync 信号到来的时候回调 onVsync 呢?答案是 FrameDisplayEventReceiver 的初始化的时候,最终通过监听文件句柄的形式,其对应的初始化流程如下
android/view/Choreographer.java
1 | private Choreographer(Looper looper, int vsyncSource) { |
android/view/Choreographer.java
1 | public FrameDisplayEventReceiver(Looper looper, int vsyncSource) { |
android/view/DisplayEventReceiver.java
1 | public DisplayEventReceiver(Looper looper, int vsyncSource) { |
nativeInit 后续的代码可以自己跟一下,可以对照这篇文章和源码,由于篇幅比较多,这里就不细写了(https://www.jianshu.com/p/304f56f5d486) , 后续梳理好这一块的逻辑后,会在另外的文章更新。
简单来说,FrameDisplayEventReceiver 的初始化过程中,通过 BitTube(本质是一个 socket pair),来传递和请求 Vsync 事件,当 SurfaceFlinger 收到 Vsync 事件之后,通过 appEventThread 将这个事件通过 BitTube 传给 DisplayEventDispatcher ,DisplayEventDispatcher 通过 BitTube 的接收端监听到 Vsync 事件之后,回调 Choreographer.FrameDisplayEventReceiver.onVsync ,触发开始一帧的绘制,如下图
Choreographer 处理绘制的逻辑核心在 Choreographer.doFrame 函数中,从下图可以看到,FrameDisplayEventReceiver.onVsync post 了自己,其 run 方法直接调用了 doFrame 开始一帧的逻辑处理
android/view/Choreographer.java
1 | public void onVsync(long timestampNanos, long physicalDisplayId, int frame) { |
doFrame 函数主要做下面几件事
1 | void doFrame(long frameTimeNanos, int frame) { |
Choreographer.doFrame 的掉帧检测比较简单,从下图可以看到,Vsync 信号到来的时候会标记一个 start_time ,执行 doFrame 的时候标记一个 end_time ,这两个时间差就是 Vsync 处理时延,也就是掉帧
我们以 Systrace 的掉帧的实际情况来看掉帧的计算逻辑
这里需要注意的是,这种方法计算的掉帧,是前一帧的掉帧情况,而不是这一帧的掉帧情况,这个计算方法是有缺陷的,会导致有的掉帧没有被计算到
Choreographer 中 FrameInfo 来负责记录帧的绘制信息,doFrame 执行的时候,会把每一个关键节点的绘制时间记录下来,我们使用 dumpsys gfxinfo 就可以看到。当然 Choreographer 只是记录了一部分,剩余的部分在 hwui 那边来记录。
从 FrameInfo 这些标志就可以看出记录的内容,后面我们看 dumpsys gfxinfo 的时候数据就是按照这个来排列的
1 | // Various flags set to provide extra metadata about the current frame |
doFrame 函数记录从 Vsync time 到 markPerformTraversalsStart 的时间
1 | void doFrame(long frameTimeNanos, int frame) { |
1 | void doFrame(long frameTimeNanos, int frame) { |
Input 回调调用栈
input callback 一般是执行 ViewRootImpl.ConsumeBatchedInputRunnable
android/view/ViewRootImpl.java
1 | final class ConsumeBatchedInputRunnable implements Runnable { |
Input 时间经过处理,最终会传给 DecorView 的 dispatchTouchEvent,这就到了我们熟悉的 Input 事件分发
Animation 回调调用栈
一般我们接触的多的是调用 View.postOnAnimation 的时候,会使用到 CALLBACK_ANIMATION
1 | public void postOnAnimation(Runnable action) { |
那么一般是什么时候回调用到 View.postOnAnimation 呢,我截取了一张图,大家可以自己去看一下,接触最多的应该是 startScroll,Fling 这种操作
其调用栈根据其 post 的内容,下面是桌面滑动松手之后的 fling 动画。
另外我们的 Choreographer 的 FrameCallback 也是用的 CALLBACK_ANIMATION
1 | public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { |
Traversal 调用栈
1 | void scheduleTraversals() { |
doTraversal 的 TraceView 示例
由于动画、滑动、Fling 这些操作的存在,我们需要一个连续的、稳定的帧率输出机制。这就涉及到了 Vsync 的请求逻辑,在连续的操作,比如动画、滑动、Fling 这些情况下,每一帧的 doFrame 的时候,都会根据情况触发下一个 Vsync 的申请,这样我们就可以获得连续的 Vsync 信号。
看下面的 scheduleTraversals 调用栈(scheduleTraversals 中会触发 Vsync 请求)
我们比较熟悉的 invalidate 和 requestLayout 都会触发 Vsync 信号请求
我们下面以 Animation 为例,看看 Animation 是如何驱动下一个 Vsync ,来持续更新画面的
android/animation/ObjectAnimator.java
1 | public void start() { |
android/animation/ValueAnimator.java
1 | private void start(boolean playBackwards) { |
android/animation/AnimationHandler.java
1 | public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) { |
调用 postFrameCallback 会走到 mChoreographer.postFrameCallback ,这里就会触发 Choreographer 的 Vsync 请求逻辑
android/animation/AnimationHandler.java
1 | public void postFrameCallback(Choreographer.FrameCallback callback) { |
android/view/Choreographer.java
1 | private void postCallbackDelayedInternal(int callbackType, |
通过上面的 Animation.start 设置,利用了 Choreographer.FrameCallback 接口,每一帧都去请求下一个 Vsync
动画过程中一帧的 TraceView 示例
Choreographer 是线程单例的,而且必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和 Looper 绑定,一般是 App 主线程的 Looper 绑定
DisplayEventReceiver 是一个 abstract class,其 JNI 的代码部分会创建一个IDisplayEventConnection 的 Vsync 监听者对象。这样,来自 AppEventThread 的 VSYNC 中断信号就可以传递给 Choreographer 对象了。当 Vsync 信号到来时,DisplayEventReceiver 的 onVsync 函数将被调用。
DisplayEventReceiver 还有一个 scheduleVsync 函数。当应用需要绘制UI时,将首先申请一次 Vsync 中断,然后再在中断处理的 onVsync 函数去进行绘制。
Choreographer 定义了一个 FrameCallback interface,每当 Vsync 到来时,其 doFrame 函数将被调用。这个接口对 Android Animation 的实现起了很大的帮助作用。以前都是自己控制时间,现在终于有了固定的时间中断。
Choreographer 的主要功能是,当收到 Vsync 信号时,去调用使用者通过 postCallback 设置的回调函数。目前一共定义了五种类型的回调,它们分别是:
ListView 的 Item 初始化(obtain\setup) 会在 input 里面也会在 animation 里面,这取决于
CALLBACK_INPUT 、CALLBACK_ANIMATION 会修改 view 的属性,所以要先与 CALLBACK_TRAVERSAL 执行
由于 Choreographer 的位置,许多性能监控的手段都是利用 Choreographer 来做的,除了自带的掉帧计算,Choreographer 提供的 FrameCallback 和 FrameInfo 都给 App 暴露了接口,让 App 开发者可以通过这些方法监控自身 App 的性能,其中常用的方法如下:
1 | public interface FrameCallback { |
1 | Choreographer.getInstance().postFrameCallback(youOwnFrameCallback ); |
1 | public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) { |
TinyDancer 就是使用了这个方法来计算 FPS (https://github.com/friendlyrobotnyc/TinyDancer)
adb shell dumpsys gfxinfo
1 | Window: StatusBar |
命令解释:
数据:
掉帧 jank 计算
每一行都可以通过下面的公式得到一个值,该值是一个标准,我们称为jankflag,如果当前行的jankflag与上一行的jankflag发生改变,那么就叫掉帧
ceil((C - A) / refresh-period)
1 | Parcel data = Parcel.obtain(); |
SKIPPED_FRAME_WARNING_LIMIT 默认为30 , 由 debug.choreographer.skipwarning 这个属性控制
1 | if (jitterNanos >= mFrameIntervalNanos) { |
Blockcanary 做性能监控使用的是 Looper 的消息机制,通过对 MessageQueue 中每一个 Message 的前后进行记录,打到监控性能的目的
android/os/Looper.java
1 | public static void loop() { |
所谓的异步消息其实就是这样的,我们可以通过 enqueueBarrier 往消息队列中插入一个 Barrier,那么队列中执行时间在这个 Barrier 以后的同步消息都会被这个 Barrier 拦截住无法执行,直到我们调用 removeBarrier 移除了这个 Barrier,而异步消息则没有影响,消息默认就是同步消息,除非我们调用了 Message 的 setAsynchronous,这个方法是隐藏的。只有在初始化 Handler 时通过参数指定往这个 Handler 发送的消息都是异步的,这样在 Handler 的 enqueueMessage 中就会调用 Message 的 setAsynchronous 设置消息是异步的,从上面 Handler.enqueueMessage 的代码中可以看到。
所谓异步消息,其实只有一个作用,就是在设置 Barrier 时仍可以不受 Barrier 的影响被正常处理,如果没有设置 Barrier,异步消息就与同步消息没有区别,可以通过 removeSyncBarrier 移除 Barrier
scheduleTraversals 的时候 postSyncBarrier
1 | void scheduleTraversals() { |
doTraversal 的时候 removeSyncBarrier
1 | void doTraversal() { |
Choreographer post Message 的时候,会把这些消息设为 Asynchronous ,这样 Choreographer 中的这些 Message 的优先级就会比较高,
1 | Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action); |
系统厂商由于可以直接修改源码,也利用这方面的便利,做一些功能和优化,不过由于保密的问题,代码就不直接放上来了,我可以大概说一下思路,感兴趣的可以私下讨论
Choreographer 本身是没有 input 消息的, 不过修改源码之后,input 消息可以直接给到 Choreographer 这里, 有了这些 Input 消息,Choreographer 就可以做一些事情,比如说提前响应,不去等 Vsync
当一个 Android App 退到后台之后,只要他没有被杀死,那么他做什么事情大家都不要奇怪,因为这就是 Android。有的 App 退到后台之后还在持续调用 Choreographer 中的 Animation Callback,而这个 Callback 的执行完全是无意义的,而且用户还不知道,但是对 cpu 的占用是比较高的。
所以在 Choreographer 中会针对这种情况做优化,禁止不符合条件的 App 在后台继续无用的操作
和移动事件优化一样,由于有了 Input 事件的信息,在某些场景下我们可以通知 SurfaceFlinger 不用去等待 Vsync 直接做合成操作
我们前面说,主线程的所有操作都是给予 Message 的 ,如果某个操作,非重要的 Message 被排列到了队列后面,那么对这个操作产生影响;而通过重新排列 MessageQueue,在应用启动的时候,把启动相关的重要的启动 Message 放到队列前面,来起到加快启动速度的作用
90/120 fps 的手机上 , Vsync 间隔从 16.6ms 变成了 11.1ms/8.8ms ,这带来了巨大的性能和功耗挑战,如何在一帧内完成渲染的必要操作,是手机厂商必须要思考和优化的地方:
由于博留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 基于 Choreographer 的渲染机制详解
掘金 - Android 基于 Choreographer 的渲染机制详解
一个人可以走的更快 , 一群人可以走的更远
]]>
随着 Android 系统版本的更迭 , 以及 App 的代码膨胀 , Android 系统对内存的需求越来越大 , 但是目前市面上还存在着大量的 4G 内存以下的机器 , 这部分用户就很容易遇到整机低内存的情况 , 尤其是在系统大版本更新和 App 越装越多的情况下 .
Android 低内存会导致性能问题 , 具体表现就是响应慢和卡顿 . 比如启动一个应用要花比平时更长的时间 ; 滑动列表会掉更多帧 ; 后台的进程减少导致冷启动变多 ; 手机很容易发热发烫等 , 下面我会概述发生这些性能问题的原因 . Debug 的方法 , 以及可能的优化措施 .
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇
最简单的方法是使用 Android 系统自带的 Dumpsys meminfo 工具
1 | adb shell dumpsys meminfo |
如果系统处于低内存的话 , 会有如下特征:
低内存的时候, LKMD 会非常活跃, 在 Kernel Log 里面可以看到 LMK 杀进程的信息:
1 | [kswapd0] lowmemorykiller: Killing 'u.mzsyncservice' (15609) (tgid 15609), adj 906, |
上面这段 Log 的意思是说, 由于 mem 低于我们设定的 900 的水位线 (261272kB),所以把 pid 为 15609 的 mzsyncservice 这个进程杀掉(这个进程的 adj 是 906 )
这里是 Linux Kernel 展示 meminfo 的地方 , 关于 meminfo 的解读,可以参考这篇文章:/PROC/MEMINFO之谜
从结果来 , 当系统处于低内存的情况时候 , MemFree 和 MemAvailable 的值都很小
1 | MemTotal: 5630104 kB |
低内存的时候,整机使用的时候要比非低内存的时候要卡很多,点击应用或者启动 App 都会有不顺畅或者响应慢的感觉
主线程出现大量的 IO 相关的问题 ,
Linux 系统的 page cache 链表中有时会出现一些还没准备好的 page ( 即还没把磁盘中的内容完全地读出来 ) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.
当出现大量的 IO 操作的时候,应用主线程的 Uninterruptible Sleep 也会变多,此时涉及到 io 操作(比如 view ,读文件,读配置文件、读 odex 文件),都会触发 Uninterruptible Sleep , 导致整个操作的时间变长
低内存会触发 Low Memory Killer 进程频繁进行扫描和杀进程,kswapd0 是一个内核工作线程,内存不足时会被唤醒,做内存回收的工作。 当内存频繁在低水位的时候,kswapd0 会被频繁唤醒,占用 cpu ,造成卡顿和耗电。
比如下面这个情况, kswapd0 占用了 855 的超大核 cpu7 ,而且是满频在跑,耗电可想而知,如果此时前台应用的主线程跑到了 cpu7 上,很大可能会出现 cpu 竞争,导致调度不到而丢帧。
HeapTaskDaemon 通常也会在低内存的时候跑的很高
, 来做内存相关的操作
对 AMS 的影响主要集中在进程的查杀上面 , 由于 LMK 的介入 , 处于 Cache 状态的进程很容易被杀掉 , 然后又被他们的父进程或者其他的应用所拉起来 , 导致陷入了一种死循环 . 对系统 CPU \ Memory \ IO 等资源的影响非常大.
比如下面就是一次 Monkey 之后的结果 , QQ 在短时间内频繁被杀和重启 .
1 | 07-23 14:32:16.969 1435 3420 I am_proc_bound: [0,30387,com.tencent.mobileqq] |
其对应的 Systrace - SystemServer 中可以看到 AM 在频繁杀 QQ 和起 QQ
此 Trace 对应的 Kernel 部分也可以看到繁忙的 cpu
手机经过长时间老化使用整机卡顿一下 , 或者整体比刚刚开机的时候操作要慢 , 可能是因为触发了内存回收或者 block io , 而这两者又经常有关联 . 内存回收可能触发了 fast path 回收 \ kswapd 回收 \ direct reclaim 回收 \ LMK杀进程回收等。(fast path 回收不进行回写)
回收的内容是匿名页 swapout 或者 file-backed 页写回和清空。(假设手机都是 swap file 都是内存,不是 disk), 涉及到 file 的,都可能操作 io,增加 block io 的概率。
还有更常见的是打开之前打开过的应用,没有第一次打开的快,需要加载或者卡一段时间 . 可能发生了 do_page_fault,这条路径经常见到 block io 在 wait_on_page_bit_killable(),如果是 swapout 内存,就要 swapin 了。如果是普通文件,就要 read out in pagecache/disk.
do_page_fault —> lock_page_or_retry -> wait_on_page_bit_killable 里面会判断 page 是否置位 PG_locked, 如果置位就一直阻塞, 直到 PG_locked 被清除 , 而 PG_locked 标志位是在回写开始时和 I/O 读完成时才会被清除,而 readahead 到 pagecache 功能也对 block io 产生影响,太大了增加阻塞概率。
下面这个 Trace 是低内存情况下 , 抓取的一个 App 的冷启动 , 我们只取应用启动到第一帧显示的部分 ,总耗时为2s 。
可以看到其 Running 的总时间是 682 ms ,
低内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 共花费了 2s . 从下面的 Thread 信息那里可以看到
从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程也非常多 , 比如若干个 kworker 和 kswapd0.
正常内存情况下 , 这个 App 从 bindApplication 到第一帧显示 , 只需要 1.22s . 从下面的 Thread 信息那里可以看到
从这段时间内的 CPU 使用情况来看 , 除了 HeapTaskDeamon 跑的比较多之外 , 其他的内存和 io 相关的进程非常少.
下面列举的只是一些经验之谈 , 具体问题还是得具体分析 , 在 Android 平台上 , 对三方应用的管控是非常重要的 , 很多小白用户 , 一大堆常驻通知和后台服务 , 导致这些 App 的优先级非常高 , 很难被杀掉 . 导致整机的内存长时间比较低 . 所以做系统的必要的优化之后 , 就要着重考虑对三方应用的查杀和管控逻辑 , 尽量减少后台进程的个数 , 在必要的时候 , 清理掉无用的进程来释放内存个前台应用使用.
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 低内存篇
]]>一个人可以走的更快 , 一群人可以走的更远
这篇文章中分析过程包括我之前在Android 中的卡顿丢帧原因概述 - 方法论 里面提到的一些工具 , 包括 : 复现视频 \ Event Log \ Android Studio 源码和 App Debug \ Android Studio Profile \ Systrace \ Dumpsys \ PS 等 . 大多数工具大家都在开发过程中使用过 , 这次分析正是使用了这些工具相互配合 , 最终找到的问题的原因.
大家看下来可能会觉得 , 这么简单一个问题还需要写一篇文章 ? 我写这篇文章的目的一是为了记录给自己 , 二是觉得分析过程比较有普遍性 , 包括分析思路和工具的使用 , 如果可以帮助到大家 , 那么最好不过了 , 如果你也有好的思路或者独家调试技巧 , 欢迎大家扫描关于我 里面的讨论群二维码加入群聊 , 共同进步!
这个问题是测试直接报过来的 , Bug 描述是典型的按现象描述 : “从应用返回桌面 , 桌面图标加载慢“. 测试这边提供了录制的视频和抓取的 Log , 以及对应的 Systrace 等. 既然现象和 Log 都在 , 那么就开始分析吧.
1 | EventLog |
那么这里就可以简单还原问题了 , 测试报的是从应用返回桌面 , 桌面图标加载慢 , 从 Event Log 来看 , 桌面显示慢 , 是因为桌面被杀了 , 所以从 App 返回的时候 , 桌面需要重新加载 , 从桌面进程创建到桌面完全显示 , 花费了 413ms(实际到桌面完全显示,花费了至少 2s 左右,因为 Launcher 冷启动还要重新加载内容).
从上面的分析来看 , 我们需要找到 Launcher 被杀的原因 , 从现象上来看 , 似乎是和 com.jx.cmcc.ict.ibelieve 这个进程有关系 , 但是我们目前是没有办法确认的 .
这里我们重点看这个这个 Event Log
1 | am_kill : [0,13509,com.meizu.flyme.launcher,600,kill background] |
这里可以看到 Launcher 被杀的原因是 kill background , 查看对应的源码可知,reason = kill background 是 AMS.killBackgroundProcesses 这里发出的.
ActivityManagerService.killBackgroundProcesses
1 | public void killBackgroundProcesses(final String packageName, int userId) { |
对源码比较熟悉的同学可以很快知道 , AMS.killBackgroundProcesses 这个接口会提供给三方应用去调用 , 其 Binder 的客户端在 ActivityManager.killBackgroundProcesses 这里
ActivityManager.killBackgroundProcesses
1 | /** |
知道了上面的代码逻辑 , 我们需要做的就是找到在这个场景下 , 是哪个应用调用 ActivityManager.killBackgroundProcesses 杀掉了桌面. 由于不知道具体是哪个应用(这里虽然我们怀疑是 com.jx.cmcc.ict.ibelieve , 但是没有证据) , 我们先对 SystemServer 进程进行 Debug .
1.首先对源码进行 debug , 首先在 Android 中点击 debug 按钮 , 选择 system_process 这个进程(就是我们所说的 SystemServer) , 然后点击 OK . 代码的断点我们打在上面列出的 ActivityManagerService.killBackgroundProcesses 方法里面.
2.点击启动怀疑的 App ( 可以从 EventLog 和视频里面倒推,找到比较可疑的 App , 安装后进行本地测试复现 , 这里选择了视频中出现的几个应用,包括我们之前怀疑的 com.jx.cmcc.ict.ibelieve- 和我信 ) , 点击其他的应用都不会进入到这个断点, 而在点击 和我信 这个 App 启动后走到的断点
3.这里我们可以看到调用栈是一个 Binder 调用 , 我们需要找到这个 Binder 调用的客户端. 在 AS 里面继续操作 , 点击如下图的计算器按钮 , 输入 getRealCallingPid() 点击下面的 Evaluate , 就可以看到结果. result = 29771
4.通过 PS 命令 , 查看这个 pid 对应的 app
可以看到就是这个应用调用的 killBackgroundProcesses
为了进一步调查,我们对这个 app 进行 debug , 由于没有源码,我们直接把断点打到 android/app/ActivityManager.killBackgroundProcesses 这里(因为这里是客户端代码 , 所以调试 App 进程的时候 , 可以直接打断点 )
本地安装这个应用进行调试, 发现登录后,再次启动, 桌面必会被杀 ,确定就是这个 App 的问题
到了这一步我们已经基本上确定问题就是这个 App 引起的了 , 不过如果我们想看比较详细的调用情况 , 可以使用 Android Studio Profile
打开 Android Studio , 点击 Profiler 按钮 , 点击 + 号 , 选择 com.jx.cmcc.ict.ibelieve 这个进程 , 然后点击 CPU 这一栏
这里选择了 Trace Java Methods , 然后点击旁边的 Record , 就可以开始进行操作 , 操作结束后 , 点击 Stop , AS 会自动开始解析.
解析结果我们可以看这里
最下面就是刚刚操作所对应的详细函数调用栈 , 以真正运行的顺序展示在我们面前(我经常会用这个工具来查看源码逻辑和三方应用的代码逻辑 , 不管是学习还是解决问题 , 都是一个非常好的方法)
我们使用 ctrl+f 进行搜索 killBackgroundProcesses , 如果有的话 , 会以高亮显示, 我们只需要用鼠标放大就可以看到详细的调用栈
可以看到这个 App 在 loadComplete 回调里面执行了 killBackground 方法.(到了这里,应用开发者就已经知道是哪里的问题了,修复起来飞快)
如上面所示 , 调用 killBackgroundProcesses 是需要Manifest.permission.KILL_BACKGROUND_PROCESSES 这个权限的 .
1 | Manifest.permission.KILL_BACKGROUND_PROCESSES) ( |
执行 adb shell dumpsys package com.jx.cmcc.ict.ibelieve 查看 com.jx.cmcc.ict.ibelieve 这个进程所申请的权限 , 发现这个应用在安装的时候就申请了KILL_BACKGROUND_PROCESSES 这个权限 , 且默认是授予的.
1 | install permissions: |
对应的权限级别为 normal
也就是说 , 所有的第三方应用都可以默认有这个权限 , 只要你申请 . 这个案例里面 , 就是因为这个 App 申请了这个权限 , 且执行了错误的行为 , 导致把桌面杀掉 , 严重影响用户体验. Sad !
由于经常使用 Systrace , 那么 Systrace 是否可以找到元凶呢? 答案是可以 (这里如果对如何在 Systrace 上查看唤醒信息不了解 , 可以看一下这篇文章分析 Systrace 预备知识). 我们录制一段 Systrace , 安装下面的顺序去看
1.首先看 system_server 进程对应的 trace ,找到 killProcessGroup 对应的点 , 查看其唤醒情况 , 可以看到是 19688 这个线程唤醒执行 AMS 的 killProcessGroup
在 Systrace 中搜索 19688 , 可以看到是 Binder:1295_1E , 1295 就是 SystemServer
查看对应的 Binder:1295_1E , 看看是哪个线程唤醒这个线程
搜索 7289这个线程 , 可以看到这个线程就是和我信这个 App 的主线程。
查看 7289 , 确定就是 com.jx.cmcc.ict.ibelieve 这个进程 . 也就是和我信这个 App(瘤子).
这里也可以反推出来这个 Kill 是 和我信 这个 App 发起的 , 进一步确认可以使用上面 AS 的 MethodTrace
从上面的分析来看 , 这个问题是由于应用申请了不恰当的权限并错误使用对应的函数导致的一个严重影响用户使用的问题. 一般分析到这一步 , 我们的工作就基本上结束了 , 后续只需要和商店沟通 , 跟 App 开发者联系进行修改即可.
不过令我感到意外的是 android.permission.KILL_BACKGROUND_PROCESSES 这个权限 Google 居然放的这么松 , 我一直以为这个权限是要专门申请以防止 App 滥用或者卵用的(毕竟涉及到其他 App 的生死存亡).
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 框架问题分析案例 - 谁杀了桌面?
]]>一个人可以走的更快 , 一群人可以走的更远
Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化 . 目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些.
说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题. 所以这一部分我分为四篇文章去讲 , 会简单讲一下哪些原因会用户觉得卡顿丢帧 :
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇
在Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章中我们列举了系统自身原因导致的手机卡顿问题 , 这一篇文章我们主要列举一些由于 App 自身原因导致的卡顿问题. 各位用户在使用 App 的时候 , 如果遇见卡顿现象 , 先别第一时间骂手机厂商优化烂 , 先想想是不是这个 App 自己的问题.
这些实际的案例 , 很多都可以在 Systrace 上看出来 , 所以我的很多贴图都是 Systrace 上实际被发现的问题 , 如果你对 Systrace 不了解 , 可以查看这个 Systrace 系列 , 这里你只需要知道 , Systrace 从系统全局的角度 , 来展示当前系统的运行状况 , 通常被用来 Debug Android 性能问题 .
主线程执行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超时都会导致卡顿 , 下面就是一些真实的案例
这里的 uploadBitmap 主要是 upload bitmap to gpu 的操作 , 如果 bitmap 过大 , 或者每一帧内容都在变化 , 那么就需要频繁 upload , 导致渲染线程耗时.
应用本身频繁调用 buildDrawingCache 会导致主线程执行耗时从而导致卡顿 , 从下图来看, 主线程每一帧明显超过了 Vsync 周期
微信对话框有多个动态表情的时候, 也会出现这种情况导致的卡顿
如果应用在 Activity 中设置了软件渲染, 那么就不会走 hwui , 直接走 skia, 纯 cpu 进程渲染, 由于这么做会加重 UI Thread 的负载, 所以大部分情况下这种写法都会导致卡顿 , 详细技术分析可以看这篇文章 Android 中的 Hardware Layer 详解
Activity resume 的时候, 与 AMS 通信要持有 AMS 锁, 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作
这一项指的是游戏自身的绘制问题, 会导致总是不能满帧去跑, 如下图, 红框部分是SurfaceFlinger 显示掉帧, 原因是底下的游戏在绘制的时候, 刚好这一帧超过了 Vsync SF 的信号.这种一般是游戏自身的问题.
应用里面涉及到 WebView 的时候, 如果页面比较复杂, WebView 的性能就会比较差, 从而造成卡顿
如果屏幕帧率和系统的 fps 不相符 , 那么有可能会导致画面不是那么顺畅. 比如使用 90 Hz 的屏幕搭配 60 fps 的动画
部分应用由于设计比较复杂, 每一帧绘制的耗时都比较长 , 这么做的话在 60 fps 的机器上可能没有问题 , 但是在 90 fps 的机器上就会很卡, 因为从 60 -> 90 , 每帧留给应用的绘制时间从 16.6 ms 变成了 11.1 ms , 如果没有在 11.1 ms 内完成, 就会出现掉帧的情况.
如下图, 这个 App 的性能比较差, 每一帧耗时都很长
主线程操作数据库
使用 SharedPerforence 的 Commit 而不是 Apply
与 WebView 进行交互的时候, 如果 WebView 出现问题, 那么也会出现卡顿
微信文章页卡顿
RenderThread 自身比较耗时, 导致一帧的时长超过 Vsync 间隔.
渲染线程耗时过长阻塞了主线程的下一次 sync
有的 App 会产生多个 RenderThread ,在某些场景下 RenderThread 在 sync 的时候花费比较多的时间,导致主线程卡顿
1 | adb shell ps -AT | grep 10300 | grep RenderThread |
Android 原生系统是一个不断进化的过程 , 目前已经进化到了 Android Q , 每个版本都会解决非常多的性能问题 , 同时也会引进一些问题 ; 到了手机厂商这里 , 由于硬件差异和软件定制 , 会在系统中加入大量的自己的代码 , 这无疑也会影响系统的性能 . 同样由于 Android 的开放 , App 的质量和行为也影响着整机的用户体验.
本篇主要列出了 App 自身的实现问题导致的流畅性问题 , Android App 最大的问题就是质量良莠不齐 , 不同于 App Store 这样的强力管理市场 , Android App 不仅可以在 Google Play 上面进行安装 , 也可以在其他的软件市场上面安装 , 甚至可以下载安装包自行安装 , 可以说上架的门槛非常低 , 那么质量就只能由 App 开发者自己来把握了.
许多大厂的 App 质量自然不必多说 , 他们对性能和用户体验都是非常关注的 , 但也会有需求和功能过多导致的性能问题 , 比如微信就非常占内存 ; 新版本的 QQ 要比之前版本的使用起来流畅性差好多 . 中小厂的 App 就更不用说了. 再加上 Android 平台的开放性 , 需要 App 玩起来黑科技 , 什么保活 \ 相互唤醒 \ 热更新 \ 跑后台任务等 . 站在 App 开发者的角度来说这无可厚非 , 但是系统开发者则希望系统能在用户使用的时候 , 前后台 App 都能有正常的行为 , 来保证前台 App 的用户体验 . 也希望 App 开发者能重视自己 App 的性能体验 , 给用户一个好印象.
系统这边发现 App 自身的性能问题 , 且在其他厂商的手机上也是一样的表现的时候 , 通常会与 App 开发者进行联系 , 沟通一起解决 .
大家可以看看这个问题 : 当手机厂商说安卓手机性能优化的时候,他们到底在做什么
这也是流畅性的一个系列文章中的一篇 , 可以点击下面的链接查看本系列的其他文章.
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 应用篇
]]>一个人可以走的更快 , 一群人可以走的更远
Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化 . 目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些.
说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题. 所以这一部分我分为四篇文章去讲 , 会简单讲一下哪些原因会用户觉得卡顿丢帧 :
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇
下面我会列出来一些实际的卡顿案例 , 这些导致卡顿的原因都是由于 Android 系统平台的一些问题导致的 , 有些问题在开发阶段就会暴露出来 , 这一类通常会在发给用户之前就解决掉 ; 有些问题是用户在长时间使用之后才会暴露出来 , 这一类问题最多 , 但是也比较难以解决 ; 还有一些问题 , 只有非常特殊的场景或者特殊的硬件才会暴露出来 .
这些实际的案例 , 很多都可以在 Systrace 上看出来 , 所以我的很多贴图都是 Systrace 上实际被发现的问题 , 如果你对 Systrace 不了解 , 可以查看这个 Systrace 系列 , 这里你只需要知道 , Systrace 从系统全局的角度 , 来展示当前系统的运行状况 , 通常被用来 Debug Android 性能问题 .
SurfaceFlinger 负责 Surface 的合成 , 一旦 SurfaceFlinger 主线程调用超时 , 就会产生掉帧 .
SurfaceFlinger 主线程耗时会也会导致 hwc service 和 crtc 不能及时完成, 也会阻塞应用的 binder 调用, 如 dequeueBuffer \ queueBuffer 等.
下图中的 SurfaceFlinger 主线程在后半部分明显超时:
SurfaceFlinger 主线程处理不及时导致应用卡顿(第一帧卡顿,后续都为黄帧)
有的 Android 机型使用了屏下光感 , 屏下光感的实现方法也会影响 SurfaceFlinger 主线程的运行 . 屏下指纹需要频繁截图 , 来区分光线和屏幕的变化 , 进行对应的亮度变化, 但是其主线程截图的方法会导致 SurfaceFlinger 主线程被截图操作所耽误, 从而导致卡顿
hwc Service 耗时也会导致 SurfaceFlinger 下一帧不会做合成操作, 导致应用的 dequeueBuffer 和 setTransationState 方法被阻塞, 导致卡顿.
如下图, 可以看到 SurfaceFlinger 的掉帧情况, Binder 的阻塞情况 和 CRTC 的耗时情况
hwc 耗时
crtc 等待 hwc
crtc 执行耗时的结果就是 SurfaceFlinger 下一帧不会做合成操作, 导致应用的 dequeueBuffer 和 setTransationState 方法被阻塞, 导致卡顿.
如下图, 可以看到 SurfaceFlinger 的掉帧情况, Binder 的阻塞情况 和 CRTC 的耗时情况
如下图 , RenderThread 跑到了小核, 导致这一帧执行时间过长,造成卡顿图片:
如下图 , cpu 频率对性能的影响图片:
在调度器看来的低优先级任务 , 在用户这里未必是低优先级任务 , 他可能正在和 App 的主线程交互 , 或者正在和 system_server 进行交互
App 主线程或者渲染线程被 RT 进程抢占也会导致系统卡顿或者响应慢 , Google 也意识到了这个问题 , 也在尝试在应用启动的时候 , 把 App 主线程和渲染线程的优先级也设置为 RT , 不过这个属性一直没开 , 因为会导致应用启动速度变慢.
大小核调度的问题通常表现在该跑在大核的任务跑到了小核 , 或者该在小核运行的任务却持续跑到大核 ,或者错误的被绑定在了某一个核心上 .
如下图, 这是一个 CTS 问题, CTS 主线程由于被绑定到了 cpu7 , 由于 cpu7 在执行 RenderThread , 所以主线程没有调度到, 导致 CTS 失败
触发 Thermal 发热限频也有可能导致卡顿 , 这算是一种硬件级别的保护 , 如果手机已经过热 , 此时如果不进行干涉 , 那么可能会导致用户手机太烫而无法持续使用手机. 一般这个时候都会对系统的资源进行一些限制 , 比如降低 cpu\gpu 的最高频率之类的 , 这么做的话 , 势必也会对流畅性造成影响.
如果你手机非常热 , 而且变卡了 , 那么放下手机休息一会 , 查杀一下后台 , 或者重启一下手机 .
后台进程活动太多,会导致系统非常繁忙, cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 , 这也是系统这边经常会碰到的问题
dumpsys cpuinfo 可以查看一段时间内 cpu 的使用情况
当线程为 Runnable 状态的时候 , 调度器如果迟迟不能对齐进行调度 , 那么就会产生长时间的 Runnable 线程状态 , 导致错过 Vsync 而产生流畅性问题.
无关进程通常是人为定义的 , 指的是与当前前台 App 运行无关的进程 , 这些活跃进程势必会对 App 主线程的调度产生影响 , 不管这些无关进程是系统的还是 App 自身的 , 或者是其他三方 App 的.
原因同上 , 当后台任务过多的时候 , cpu 资源就会异常紧缺 , 如下图就是在系统低内存的时候 , HeapTask 和 kswapD 几乎占满了整个 cpu , 在疯狂地向系统申请内存 .
system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿
Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长.
所以在使用 Android 系统的时候 , 记得多用多任务清理后台任务.
如果出现 Input 报点不均匀或者没有报点的情况, 那么主线程由于没有收到 Input 事件, 所以不去做绘制, 也会导致卡顿
如下图 , 这是一个连续滑动的 Systrace 图 , 最下面两行是 InputReader 和 InputDispatcher , 可以看到在滑动的过程中, InputReader 和 InputDispatcher 没有读出来 Input 事件, 导致卡顿
LMK 工作时, 会占用 cpu 资源 , 其表现主要有下面几点
低内存情况下, 很容易出现主线程 IO 从而导致应用卡顿
当 SurfaceFlinger 有 GPU 合成时, 其主线程的执行时间就会变长, 也会导致合成不及时而卡顿
低内存时, kswapd 由于负载比较高 , 其 cpu 占用比较高, 且经常会跑到大核上 , 导致机器发热限频, 或者抢占主线程的 cpu 时间片
SurfaceFlinger 有时候会出现 Vsync 不均匀的情况, 不均匀指的是 Vsync 间隔会持续地变化, 一会大一会小, 就会导致用户看到的画面不均匀, 有卡顿感
如下图 , 可以明显看到 SurfaceFlinger 的 VSYNC-sf 这一行间隔是不一样的. 这种问题一般是由于 SurfaceFlinger 这边的修改或者 HWC 的修改导致的 .
三方应用如果使用 Accessibility 服务监听了 Input 事件的话, InputDispatcher 的行为就会与预期的出现偏差, 导致 InputDispatcher 没有及时把事件传给主线程导致卡顿
Android 原生系统是一个不断进化的过程 , 目前已经进化到了 Android Q , 每个版本都会解决非常多的性能问题 , 同时也会引进一些问题 ; 到了手机厂商这里 , 由于硬件差异和软件定制 , 会在系统中加入大量的自己的代码 , 这无疑也会影响系统的性能 .
上面列出的这些影响流畅性的案例 , 只是 Android 系统开发中遇到的性能问题的冰山一角 , 任何一个问题都会对用户的使用产生影响 , 这也是为什么手机厂商越来越重视系统优化 . 手机厂商非常重视开发过程中和用户使用过程中遇到的性能问题 , 并开发和提出各项优化措施 , 从硬件到软件 , 从用户行为优化到系统策略动态学习 . 这也是为什么现在的手机厂商的系统越做越好 , 质量越来越高的一个原因 , 那些不重视质量只重视设计和产品的手机厂商 , 都渐渐地被消费者淘汰了.
大家可以看看这个问题 : 当手机厂商说安卓手机性能优化的时候,他们到底在做什么
这也是流畅性的一个系列文章中的一篇 , 可以点击下面的链接查看本系列的其他文章.
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 系统篇
]]>一个人可以走的更快 , 一群人可以走的更远
目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些.
说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题. 所以这一部分我分为四篇文章去讲 , 会简单讲一下哪些原因会用户觉得卡顿丢帧 :
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇
作为手机厂商优化组的一员 , 我有必要在开始之前简单描述一下我们工作的流程 . 系统开发的过程中, 有很多引起 Android 卡顿的原因,但是用户和测试感受最直观的是正在使用的应用掉帧和不流畅 . 由于测试和用户没有办法直接确定卡顿的原因, 所以一般会直接将 Bug 提到我们这边, 所以我们的角色更像是一个卡顿问题接口人, 负责分析引起卡顿的原因, 再把 Bug 分配给对应的模块负责人去解决 , 如框架 \ App \ 多媒体 \ Display \ BSP 等.
所以直接由我们来解决的问题并不是很多, 我们更多的时候是通过专门的分析工具 , 结合源码来定位和分析问题 , 最多使用的工具如下:
确定卡顿的根本原因 , 这需要对 Android App 开发 \ Android Framework 知识 \ Display 知识 \ Linux Kernel 知识有一定的了解 , 知道基本的工作流程 , 并能熟练使用对应的工具 , 区分不同的场景 , 迅速找到问题的原因 , 然后和相关模块的负责人一起讨论优化.
对于一些系统全局性的方案则需要与对应的模块负责人一起分析和解决, 必要的时候我们也会开发一些 Feature 来解决问题 .
应用卡顿问题的原因比较多, 在数据埋点还没有完善的情况下, 更多的依赖 Systrace 来从全局的角度来分析卡顿的具体原因:
由于用户反馈的不确定性 , 和内部测试的不完备性 , 通过系统或者 App 的性能埋点数据来做分析 , 是改进系统的一个好的方法 . 一方面不用用户主动参与 , 一方面有大量的数据可以来做分析 , 看趋势 .
目前国内各大手机厂商和 App 厂商基本都有自己的 APM 平台 , 负责监控 App 或者系统的监控程度 , 来做对应的优化方案 , 比如腾讯的 Matrix 平台已经监控了下面这些内容 , 其他的 App 厂商可以直接接入
手机厂商由于有代码权限 , 所以可以采集到更多的数据 , 比如 Kernel 相关的数据 : cpu 负载 \ io 负载 \ Memory 负载 \ FSync \ 异常监控 \ 温度监控 \ 存储大小监控 等 , 每一个大项又都有几十个小项 . 所以可以监控的数据会非常多 , 遇到问题也可以从多个技术指标去分析 . 这就需要在这方面经验非常丰富的团队 , 去定义这些监控指标 , 确定最终要收集那些信息 , 收集上来的数据如何去分析等.
至于后续的优化工作 , 就考验各个厂商的研发能力了 , 正如伟琳在这篇文章:那些年,我们一起经历过的 Android 系统性能优化 所说 , 目前能力比较强的手机厂商 , 都在底层各个模块 , 结合硬件做优化 , 因为归根结底都是资源的分配 ; 而一些研发实力不是很强的厂商 , 则重点还是围绕在根据场景分配资源.
这里简单概述了一下流畅性问题的一般分析思路和分析工具 , 而且由于我的方向主要在 Framework 和 App , 所以很多东西都是从上层的角度来说的 , 想必 Kernel 优化团队会有更好的角度和分析 .
各个厂商的优化大家可以看看这篇总结 , 那些年,我们一起经历过的 Android 系统性能优化 , 华米 OV 都有涉及 , 下面摘录了一段总结 , 大家可以看看
展望一下,这里想把手机厂商分为三类:
- 一类是苹果,自己研发芯片和核心元件,有自己的OS和生态;
- 二类是三星、华为,自己研发芯片和核心元件(当然华为和三星还是有所区别),共享 Android OS 和生态,当然三星在本土化这一块做的是不如华为和其他 Top 厂商的;
- 三类是其他 Android 手机厂商,芯片和核心元件来自于不同供应商,共享 Android OS和生态;
从技术层面看:
- 苹果始终会是在性能的第一阵营,可以顺利推行从硬件到 OS 到 APP 级别的任何性能保障方案;
- 三星、华为属于第二阵营,可以实现芯片-OS层面的整合优化;
- 其他 Top Android 手机厂商差距不会太大,他们有多个不同的 SoC 供应商,方案有差异,非常芯片底层的地方,往往不会去涉及,更多是做纯软件层面的策略性的优化,有价值但是不容易形成壁垒,注意这个不容易形成壁垒指的是在 top 厂商中间,一些小的厂商往往还是心有余而力不足。不过还是很期待看到有更多的突破出现。
这也是流畅性的一个系列文章中的一篇 , 可以点击下面的链接查看本系列的其他文章.
0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 方法论
]]>一个人可以走的更快 , 一群人可以走的更远
Activity 栈是一个先进后出的数据结构, 各位可以关注在每一步操作之后, 栈内容那一栏 , 可以更好地帮助理解不同的启动模式.
Demo 比较简单, 我也放到了 Github 上 , https://github.com/Gracker/AndroidLaunchModeTest , 有兴趣的可以自己跑一下 , 看看结果 , 只需要修改 StandardActivity 里面的跳转 Activity 就可以了.
1 | android:launchMode="standard" |
最基本的模式,每次启动都会创建一个新的 Activity
1 | // 1. 启动 Activity |
1 | android:launchMode="singleTop" |
如果当前 Activity 已经在栈顶,那么其 onNewIntent 会被调用;否则会重新创建 Activity
1 | // 1. 启动 MainActivity |
1 | // 1. 启动 MainActivity |
1 | android:launchMode="singleTask" |
1 | // 1. 启动 MainActivity: |
在 Manifest 中设置了 android:taskAffinity=”” 之后,启动 SingleTask 会启动一个新的 Task
1 | // 1. 启动 MainActivity |
1 | android:launchMode="singleInstance" |
单示例模式顾名思义,启动时,无论从哪里启动都会给 A 创建一个唯一的任务栈,后续的创建都不会再创建新的 A,除非 A 被销毁了
1 | taskAffinity=com.example.launchmodetest |
在 Manifest 中设置了 android:taskAffinity=”” 之后,启动 SingleInstanceActivity 会出现在多任务中 ,其余的表现与没有设置 Affinity 一致
1 | taskAffinity=null |
1 | // 1. 启动 MainActivity |
1 | taskAffinity=null |
与 Activity 有着亲和关系的任务。从概念上讲,具有相同亲和关系的 Activity 归属同一Task(从用户的角度来看,则是归属同一“ Application ”)。 Task 的亲和关系由其根 Activity 的亲和关系确定。
亲和关系确定两件事 - Activity 更改到的父项 Task(请参阅 allowTaskReparenting 属性)和通过 FLAG_ACTIVITY_NEW_TASK 标志启动 Activity 时将用来容纳它的 Task。
默认情况下,应用中的所有 Activity 都具有相同的亲和关系。您可以设置该属性来以不同方式组合它们,甚至可以将在不同应用中定义的 Activity 置于同一 Task 内。 要指定 Activity 与任何 Task 均无亲和关系,请将其设置为空字符串。
如果未设置该属性,则 Activity 继承为应用设置的亲和关系(请参阅
(From http://gityuan.com/2017/06/11/activity_record/)
下面是一个 dump 的例子,可以看到当前手机的 ActivityRecord、TaskRecord、ActivityStack
(adb shell dumpsys activity containers)
1 | /** Activity type is currently not defined. */ |
如果觉得文章有帮助, 欢迎分享到社交网站 , 希望能帮到大家.
]]>一个人可以走的更快 , 一群人可以走的更远
很多人会把 Android 中的硬件加速和 Hardware Layer 搞混,会以为启用了硬件加速,就是启用了 Hardware Layer. 所以在说 Hardware Layer 之前,我们先说一下硬件加速
关于硬件加速的比较详细的文章,推荐大家看这三篇
硬件加速,实际上应该叫 GPU 加速,软硬件加速的区别主要是图形的绘制究竟是 GPU 来处理还是 CPU,如果是 GPU,就认为是硬件加速绘制,反之,则是软件绘制
目前的 Android 版本, 默认情况下都是开了硬件加速的,如果你的 App 没有特殊声明,那么硬件加速就是默认开启的
上面三篇文章都有介绍,代码级别和原理级别都讲的比较深,这里我从 Systrace 的角度来给大家展示一下硬件加速下 App 的绘制与软件加速的区别
由于默认情况下就是硬件加速,所以我们以最常见的滑动桌面为例,看一下硬件加速情况下 App 在 Systrace 上的表现
硬件加速情况下,App 存在主线程和渲染线程,一帧的绘制是主线程和渲染线程一起配合执行的
我们把 Systrace 放大,来看每一帧主线程和渲染线程是怎么工作的,GPU 是什么时候介入工作,实现”加速”的
GPU 的真正介入是在 RenderThread 中的部分操作中
对应的,软件加速我们也找一个 App 来进行演示:云闪付
首先放一张全景图,可以看到软件渲染下,只有主线程,没有渲染线程,所有的渲染工作,都在主线程完成,同时可以看到,软件渲染下,每一帧的执行时间都非常长,超过1个 Vsync 周期,所以滑动的时候会一卡一卡的,非常难受 Systrace 下载
我们把 Systrace 放大,来看每一帧主线程是怎么工作的
通过上面的对比以及推荐的三篇文章的阅读,你应该对硬件渲染和软件渲染的区别了然于胸,这里总结一下
说完了硬件渲染,我们来说一下 Software Layer 和 Hardware Layer , 这两个概念主要是针对 View 的说的, 与此时 App 是硬件渲染还是软件渲染没有直接关系(但是有依赖关系,稍后会讲).
一个 View 的 layerType 共有三种状态( 后面的英文是官方文档,先读英文我再讲解):
默认情况下,所有的 View 都是这个 layerType,这种情况下,这个 View 不会做任何的特殊处理,该怎么走怎么走
Software layerType , 标识这个 View 有一个软件实现的 Layer ,怎么个软件实现法呢,实际上就是把这个 View,根据一定的条件,变成一个 Bitmap 对象
1 | android/view/View.java |
Hardware layerType ,标识这个 View 有一个硬件实现的 Layer ,通过第一小节我知道,这里的硬件指的是 GPU ,那么硬件实现的 Layer 顾名思义就是通过 GPU 来实现的,通常是OpenGL硬件上的帧缓冲对象或FBO(离屏渲染 Buffer)
注意:这里 Hardware layerType 是依赖硬件加速的,如果硬件加速开启,那么才会有 FBO 或者帧缓冲 ; 如果硬件加速关闭,那么就算你设置一个 View 的 LayerType 是 Hardware Layer ,也会按照 Software Layer 去做处理
而设置 Hardware Layer 对 alpha\translation \ scale \ rotation \ 这几个属性动画性能有帮助(同样的, 设置 Software Layer 也有相同的功效,下面的小例子环节会有详细的讲解),具体的使用如下
动画开始前,设置 LayerType 为 LAYER_TYPE_HARDWARE(代码为官方示例)
1 | view.setLayerType(View.LAYER_TYPE_HARDWARE, null); |
动画结束的时候,重新设置为LAYER_TYPE_NONE(代码为官方示例)
1 | view.setLayerType(View.LAYER_TYPE_HARDWARE, null); |
由于 Hardware Layer 的特性,属性动画( alpha \ translation \ scale \ rotation \ )过程中只更新 View 的 property,不会每一帧都去销毁和重建 FBO,其动画性能会有很大的提升。当然这里要注意属性动画的过程中( 比如 AnimationUpdate 回调中),不要做除了上述属性更新之外的其他事情,比如添加删除子 View、修改 View 的显示内容等,这会使得 FBO 失效,性能反而变差
看 Trace 经常会有这样的情况出现 , 我们知道 Software layer 的生成过程本质上是生成一个 Bitmap Cache ,这个 Cache 的生成是很耗时的, 从下面的 Trace 也可以看出来,每一帧都比一个 Vsync 周期要长。
之所以下面的 Trace 每一帧都去调用了 buildDrawingCache/SW ,是因为每一帧的过程中,这个 View 的内容进行了更新,导致 Cache 失效,所以每一帧都去触发销毁 Cache 和重建 Cache,导致界面滑动卡顿
下面这个 Trace 是微信朋友圈的大图滑动情况 Trace 在 Github 上可以下载
放大来看,每一帧都在做 buildDrawingCache 操作,说明每一帧的缓存都失效了,在进行销毁和重建,性能极差,滑动的时候顿挫感非常严重
简单看一下 LAYER_TYPE_HARDWARE 的代码流程,详细的流程可以看上面推荐的文章
1 | buildLayer -> buildDrawingCache -> buildDrawingCacheImpl |
其中 buildDrawingCache 的实现, 可以看到对应的 Trace 就是在这里打印的:
1 | public void buildDrawingCache(boolean autoScale) { |
不正确使用 Hardware Layer 和不正确使用 Software Layer 会引起相同的性能问题,比如下面这个场景 (桌面打开文件夹),由于开发的实现问题,多个文件夹小图标都被设置了 Hardware LayerType , 导致 RenderThread 非常耗时,又因为每一帧其中的内容都在变,导致每一帧的 Hardware Layer 都失效,被销毁后重建,所以就有了下面的 Systrace 所展示的情况
我们放大 RenderThread 的一帧来看
我们可以在 设置 - 辅助功能 - 开发者选项 - 显示硬件层更新(Show hardware layers updates) 这个工具来追踪硬件层更新导致的性能问题 。
当 View 渲染 Hardware Layer 的时候整个界面会闪烁绿色,正常情况下,它应该在动画开始的时候闪烁一次(也就是 Layer 渲染初始化的时候),后续的动画不应该再有绿色出现;如果你的 View 在整个动画期间保持绿色不变,这就是持续的缓存失效问题了
查看 Systrace 也可以发现相同的问题, 两个工具可以一起使用,早些发现动画的性能问题。
为了说明上面所说的情况,我们用一个小例子来做示例,演示在各种情况下,其性能表现,代码非常简单(代码项目地址 :https://github.com/Gracker/Android_HardwareLayer_Example), 项目 Systrace 文件夹中包含此文章中涉及的所有例子(这都是好东西,值得收藏)
1 | //设置动画 |
为了得到准确的数据,我们使用 gfxinfo 得到的数据来进行对比( adb shell dumpsys gfxinfo)
gfxInfo 记录的是每一帧的耗时,我们重点看下面几个指标
1 | AnimatorListener 和 AnimatorUpdateListener 都不重写, 如下,函数内的都注释掉 |
可以看到有部分黄帧, 渲染线程中 flush commands 方法执行比较久Systrace 下载
可以看到 Janky Frames 比例为 46%,99th percentile: 32ms ,说明性能比较差,同时 Number High input latency = 30 说明主线程的负载是比较高的
1 | Total frames rendered: 30 |
1 | objectAnimator1.addListener(new Animator.AnimatorListener() { |
第一帧执行 buildDrawingCache/SW Layer for AppCompatTextView ,后续的属性动画中,都没有在执行这个方法,可以看到动画过程中所有的帧都是绿色,说明性能很好Systrace 下载
可以看到 Janky Frames 比例为 3%,99th percentile: 16ms ,说明性能非常好,同时 Number High input latency = 0 说明主线程的负载是比较低的
1 | Total frames rendered: 31 |
1 | objectAnimator1.addListener(new Animator.AnimatorListener() { |
可以看到,动画过程全是绿帧,性能非常好Systrace 下载
可以看到 Janky Frames 比例为 0%,99th percentile: 14ms ,说明性能非常好,同时 Number High input latency = 0 说明主线程的负载是非常低的
1 | Total frames rendered: 31 |
1 | objectAnimator1.addListener(new Animator.AnimatorListener() { |
可以看到动画过程中有部分黄帧,部分帧的 Animation、measure、layout、draw 比较耗时Systrace 下载
可以看到 Janky Frames 比例为 38%,99th percentile: 29ms ,说明性能比较差,同时 Number High input latency = 31 说明主线程的负载是比较高的
1 | Total frames rendered: 31 |
1 | objectAnimator1.addListener(new Animator.AnimatorListener() { |
由于每一帧都在更新内容,所以每次 buildDrawingCache 生成的 Bitmap 都会被销毁和重建,此时的瓶颈都在主线程中,由于 buildDrawingCache 每一帧都执行,导致 Animation 和 Draw 的执行时间都很长Systrace 下载
可以看到 Janky Frames 比例为 41%,99th percentile: 32ms ,说明性能比较差,同时 Number High input latency = 18 说明主线程的负载是比较高的
1 | Total frames rendered: 29 |
1 | objectAnimator1.addListener(new Animator.AnimatorListener() { |
与 Software Layer 情况类似,由于每一帧都在更新内容,所以每次 drawLayer 生成的 Buffer 都会被销毁和重建,此时的瓶颈都在主线程 + 渲染线程中,由于每一帧内容更新和 Buffer 销毁重建,导致主线程和渲染线程执行时间都很长,性能比较差Systrace 下载
可以看到 Janky Frames 比例为 46%,99th percentile: 32ms ,说明性能比较差,同时 Number High input latency = 30 说明主线程的负载是比较高的
1 | Total frames rendered: 30 |
从上面的六个案例可以看到,相同的动画,在不同的 LayerType 之下,其性能表现差别很大,这还只是简单的属性动画,如果碰到更加复杂的动画,性能差别会更大。
我们对上面几个案例和表现出来的性能数据做一下简单的总结:
既然读完了,如果有什么想法可以留言沟通,也可以扫文章下面的微信二维码加好友一起讨论;如有疏漏或者错误的地方,辛苦大家告知一下,我尽早更新以免误导他人;如果觉得有用,也请把这篇文章分享给其他人.
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的 Hardware Layer 详解
]]>一个人可以走的更快 , 一群人可以走的更远
本文介绍了如何查看 Systrace 中的线程状态 , 如何对线程的唤醒信息进行分析, 如何解读信息区的数据, 以及介绍了常用的快捷键. 通过本篇文章的学习, 相信你可以掌握进程和线程相关的一些信息, 也知道如何查看复杂的 Systrace 中包含的关键信息
Systrace 会用不同的颜色来标识不同的线程状态, 在每个方法上面都会有对应的线程状态来标识目前线程所处的状态,通过查看线程状态我们可以知道目前的瓶颈是什么, 是 cpu 执行慢还是因为 Binder 调用, 又或是进行 io 操作, 又或是拿不到 cpu 时间片
线程状态主要有下面几个
只有在该状态的线程才可能在 cpu 上运行。而同一时刻可能有多个线程处于可执行状态,这些线程的 task_struct 结构被放入对应 cpu 的可执行队列中(一个线程最多只能出现在一个 cpu 的可执行队列中)。调度器的任务就是从各个 cpu 的可执行队列中分别选择一个线程在该cpu 上运行
作用:我们经常会查看 Running 状态的线程,查看其运行的时间,与竞品做对比,分析快或者慢的原因:
线程可以运行但当前没有安排,在等待 cpu 调度
作用:Runnable 状态的线程状态持续时间越长,则表示 cpu 的调度越忙,没有及时处理到这个任务:
线程没有工作要做,可能是因为线程在互斥锁上被阻塞。
作用 : 这里一般是在等事件驱动
线程在I / O上被阻塞或等待磁盘操作完成,一般底线都会标识出此时的 callsite :wait_on_page_locked_killable
作用:这个一般是标示 io 操作慢,如果有大量的橘色不可中断的睡眠态出现,那么一般是由于进入了低内存状态,申请内存的时候触发 pageFault, linux 系统的 page cache 链表中有时会出现一些还没准备好的 page(即还没把磁盘中的内容完全地读出来) , 而正好此时用户在访问这个 page 时就会出现 wait_on_page_locked_killable 阻塞了. 只有系统当 io 操作很繁忙时, 每笔的 io 操作都需要等待排队时, 极其容易出现且阻塞的时间往往会比较长.
线程在另一个内核操作(通常是内存管理)上被阻塞。
作用:一般是陷入了内核态,有些情况下是正常的,有些情况下是不正常的,需要按照具体的情况去分析
Systrace 会标识出一个非常有用的信息,可以帮助我们进行线程调用等待相关的分析。
一个线程被唤醒的信息往往比较重要,知道他被谁唤醒,那么我们也就知道了他们之间的调用等待关系,如果一个线程出现一段比较长的 sleep 情况,然后被唤醒,那么我们就可以去看是谁唤醒了这个线程,对应的就可以查看唤醒者的信息,看看为什么唤醒者这么晚才唤醒。
一个常见的情况是:应用主线程程使用 Binder 与 SystemServer 的 AMS 进行通信,但是恰好 AMS 的这个函数正在等待锁释放(或者这个函数本身执行时间很长),那么应用主线程就需要等待比较长的时间,那么就会出现性能问题,比如响应慢或者卡顿,这就是为什么后台有大量的进程在运行,或者跑完 Monkey 之后,整机性能会下降的一个主要原因
另外一个场景的情况是:应用主线程在等待此应用的其他线程执行的结果,这时候线程唤醒信息就可以用来分析主线程到底被哪个线程 Block 住了,比如下面这个场景,这一帧 doFrame 执行了 152ms,有明显的异常,但是大部分时间是在 sleep
这时候放大来看,可以看到是一段一段被唤醒的,这时候点击图中的 runnable ,下面的信息区就会出现唤醒信息,可以顺着看这个线程到底在做什么
20424 线程是 RenderHeartbeat,这就牵扯到了 App 自身的代码逻辑,需要 App 自己去分析 RenderHeartbeat 到底做了什么事情
Systrace 可以标示出这个的一个原因是,一个任务在进入 Running 状态之前,会先进入 Runnable 状态进行等待,而 Systrace 会把这个状态也标示在 Systrace 上(非常短,需要放大进行看)
拉到最上面查看对应的 cpu 上的 taks 信息,会标识这个 task 在被唤醒之前的状态:
顺便贴一下 Linux 常见的进程状态
位于整个 Systrace 最上面的部分,标识了 Rendering Response 和 Input Response
快捷键的使用可以加快查看 Systrace 的速度,下面是一些常用的快捷键
W : 放大 Systrace , 放大可以更好地看清局部细节
S : 缩小 Systrace, 缩小以查看整体
A : 左移
D : 右移
M : 高亮选中当前鼠标点击的段(这个比较常用,可以快速标识出这个方法的左右边界和执行时间,方便上下查看)
鼠标模式快捷切换 : 主要是针对鼠标的工作模式进行切换 , 默认是 1 ,也就是选择模式,查看 Systrace 的时候,需要经常在各个模式之间切换 , 所以点击切换模式效率比较低,直接用快捷键切换效率要高很多
数字键1 : 切换到 Selection 模式 , 这个模式下鼠标可以点击某一个段查看其详细信息, 一般打开 Systrace 默认就是这个模式 , 也是最常用的一个模式 , 配合 M 和 ASDW 可以做基本的操作
数字键2 : 切换到 Pan 模式 , 这个模式下长按鼠标可以左右拖动, 有时候会用到
数字键3 : 切换到 Zoom 模式 , 这个模式下长按鼠标可以放大和缩小, 有时候会用到
数字键4 : 切换到 Timing 模式 , 这个模式下主要是用来衡量时间的,比如选择一个起点, 选择一个终点, 查看起点和终点这中间的操作所花费的时间.
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
知乎 - Systrace 基础知识 – 分析 Systrace 预备知识
掘金 - Systrace 基础知识 – 分析 Systrace 预备知识
]]>一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
Systrace 中的 SystemServer 一个比较重要的地方就是窗口动画,由于窗口归 SystemServer 来管,那么窗口动画也就是由 SystemServer 来进行统一的处理,其中涉及到两个比较重要的线程,Android.Anim 和 Android.Anim.if 这两个线程,这两个线程的基本知识在下面有讲。
这里我们以应用启动为例,查看窗口时如何在两个线程之间进行切换(Android P 里面,应用的启动动画由 Launcher 和应用自己的第一帧组成,之前是在 SystemServer 里面的,现在多任务的动画为了性能部分移到了 Launcher 去实现)
首先我们点击图标启动应用的时候,由于 App 还在启动,Launcher 首先启动一个 StartingWindow,等 App 的第一帧绘制好了之后,再切换到 App 的窗口动画
Launcher 动画
此时对应的,App 正在启动
从上图可以看到,应用第一帧已经准备好了,接下来看对应的 SystemServer ,可以看到应用启动第一帧绘制完成后,动画切换到 App 的 Window 动画
AMS 和 WMS 算是 SystemServer 中最繁忙的两个 Service 了,与 AMS 相关的 Trace 一般会用 TRACE_TAG_ACTIVITY_MANAGER 这个 TAG,在 Systrace 中的名字是 ActivityManager
下面是启动一个新的进程的时候,AMS 的输出
在进程和四大组件的各种场景一般都会有对应的 Trace 点来记录,比如大家熟悉的 ActivityStart、ActivityResume、activityStop 等,这些 Trace 点有一些在应用进程,有一些在 SystemServer 进程,所以大家在看 Activity 相关的代码逻辑的时候,需要不断在这两个进程之间进行切换,这样才能从一个整体的角度来看应用的状态变化和 SystemServer 在其中起到的作用。
与 WMS 相关的 Trace 一般会用 TRACE_TAG_WINDOW_MANAGER 这个 TAG,在 Systrace 中 WindowManagerService 在 SystemServer 中多在对应的 Binder 中出现,比如下面应用启动的时候,relayoutWindow 的 Trace 输出
在 Window 的各种场景一般都会有对应的 Trace 点来记录,比如大家熟悉的 relayoutWIndow、performLayout、prepareToDisplay 等
Input 是 SystemServer 线程里面非常重要的一部分,主要是由 InputReader 和 InputDispatcher 这两个 Native 线程组成,关于这一部分在 Systrace 基础知识 - Input 解读 里面已经详细讲过,这里就不再详细讲了
SystemServer 由于提供大量的基础服务,所以进程间的通信非常繁忙,且大部分通信都是通过 Binder ,所以 Binder 在 SystemServer 中的作用非常关键,很多时候当后台有大量的 App 存在的时候,SystemServer 就会由于 Binder 通信和锁竞争,导致系统或者 App 卡顿。关于这一部分在 Binder 和锁竞争解读 里面已经详细讲过,这里就不再详细讲了
com/android/internal/os/BackgroundThread.java
1 | private BackgroundThread() { |
Systrace 中的 BackgroundThread
BackgroundThread 在系统中使用比较多,许多对性能没有要求的任务,一般都会放到 BackgroundThread 中去执行
ServiceThread 继承自 HandlerThread ,下面介绍的几个工作线程都是继承自 ServiceThread ,分别实现不同的功能,根据线程功能不同,其线程优先级也不同:UIThread、IoThread、DisplayThread、AnimationThread、FgThread、SurfaceAnimationThread
每个 Thread 都有自己的 Looper 、Thread 和 MessageQueue,互相不会影响。Android 系统根据功能,会使用不同的 Thread 来完成。
com/android/server/UiThread.java
1 | private UiThread() { |
Systrace 中的 UiThread
UiThread 被使用的地方如下,具体的功能可以自己去源码里面查看,关键字是 UiThread.get()
com/android/server/IoThread.java
1 | private IoThread() { |
IoThread 被使用的地方如下,具体的功能可以自己去源码里面查看,关键字是 IoThread.get()
com/android/server/DisplayThread.java
1 | private DisplayThread() { |
Systrace 中的 DisplayThread
com/android/server/AnimationThread.java
1 | private AnimationThread() { |
Systrace 中的 AnimationThread
AnimationThread 在源码中的使用,可以看到 WindowAnimator 的动画执行也是在 AnimationThread 线程中的,Android P 增加了一个 SurfaceAnimationThread 来分担 AnimationThread 的部分工作,来提高 WindowAnimation 的动画性能
com/android/server/FgThread.java
1 | private FgThread() { |
Systrace 中的 FgThread
FgThread 在源码中的使用,可以自己搜一下,下面是具体的使用的一个例子
1 | FgThread.getHandler().post(() -> { |
1 | com/android/server/wm/SurfaceAnimationThread.java |
Systrace 中的 SurfaceAnimationThread
SurfaceAnimationThread 的名字叫 android.anim.lf , 与 android.anim 有区别,
这个 Thread 主要是执行窗口动画,用于分担 android.anim 线程的一部分动画工作,减少由于锁导致的窗口动画卡顿问题,具体的内容可以看这篇文章:Android P——LockFreeAnimation
1 | SurfaceAnimationRunner( AnimationFrameCallbackProvider callbackProvider, |
]]>一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
Systrace 是 Android4.1 中新增的性能数据采样和分析工具。它可帮助开发者收集 Android 关键子系统(如 SurfaceFlinger/SystemServer/Kernel/Input/Display 等 Framework 部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。
Systrace 的功能包括跟踪系统的 I/O 操作、内核工作队列、CPU 负载以及 Android 各个子系统的运行状况等。在 Android 平台中,它主要由3部分组成:
Note : 最新版本的 platform-tools 里面已经移除了 Systrace 工具,Google 推荐使用 Perfetto 来抓 Trace
个人建议:Systrace 和 Perfetto 都可以用,哪个用着顺手就用哪个,不过最终 Google 是要用 Perfetto 来替代 Systrace 的,所以可以把默认的 Trace 打开工具切换成 Perfetto UI
Perfetto 相比 Systrace 最大的改进是可以支持长时间数据抓取,这是得益于它有一个可在后台运行的服务,通过它实现了对收集上来的数据进行 Protobuf 的编码并存盘。从数据来源来看,核心原理与 Systrace 是一致的,也都是基于 Linux 内核的 Ftrace 机制实现了用户空间与内核空间关键事件的记录(ATRACE、CPU 调度)。Systrace 提供的功能 Perfetto 都支持,由此才说 Systrace 最终会被 Perfetto 替代。
Perfetto 所支持的数据类型、获取方法,以及分析方式上看也是前所未有的全面,它几乎支持所有的类型与方法。数据类型上通过 ATRACE 实现了 Trace 类型支持,通过可定制的节点读取机制实现了 Metric 类型的支持,在 UserDebug 版本上通过获取 Logd 数据实现了 Log 类型的支持。
你可以通过 Perfetto.dev 网页、命令行工具手动触发抓取与结束,通过设置中的开发者选项触发长时间抓取,甚至你可以通过框架中提供的 Perfetto Trigger API 来动态开启数据抓取,基本上涵盖了我们在项目上能遇到的所有的情境。
在数据分析层面,Perfetto 提供了类似 Systrace 操作的数据可视化分析网页,但底层实现机制完全不同,最大的好处是可以支持超大文件的渲染,这是 Systrace 做不到的(超过 300M 以上时可能会崩溃、可能会超卡)。在这个可视化网页上,可以看到各种二次处理的数据、可以执行 SQL 查询命令、甚至还可以看到 logcat 的内容。Perfetto Trace 文件可以转换成基于 SQLite 的数据库文件,既可以现场敲 SQL 也可以把已经写好的 SQL 形成执行文件。甚至你可以把他导入到 Jupyter 等数据科学工具栈,将你的分析思路分享给其他伙伴。
比如你想要计算 SurfaceFlinger 线程消耗 CPU 的总量,或者运行在大核中的线程都有哪一些等等,可以与领域专家合作,把他们的经验转成 SQL 指令。如果这个还不满足你的需求, Perfetto 也提供了 Python API,将数据导出成 DataFrame 格式近乎可以实现任意你想要的数据分析效果。
可以在 Android 性能优化的术、道、器 这篇文章中查看各个工具的介绍和使用,以及他们的优劣比。
笔者后续会持续更新 Perfetto 系统,后续所有的图例也都会用 Perfetto 来演示。
在系统的一些关键操作(比如 Touch 操作、Power 按钮、滑动操作等)、系统机制(input 分发、View 绘制、进程间通信、进程管理机制等)、软硬件信息(CPU 频率信息、CPU 调度信息、磁盘信息、内存信息等)的关键流程上,插入类似 Log 的信息,我们称之为 TracePoint(本质是 Ftrace 信息),通过这些 TracePoint 来展示一个核心操作过程的执行时间、某些变量的值等信息。然后 Android 系统把这些散布在各个进程中的 TracePoint 收集起来,写入到一个文件中。导出这个文件后,Systrace 通过解析这些 TracePoint 的信息,得到一段时间内整个系统的运行信息。
Android 系统中,一些重要的模块都已经默认插入了一些 TracePoint,通过 TraceTag 来分类,其中信息来源如下
这样 Systrace 就可以把 Android 上下层的所有信息都收集起来并集中展示,对于 Android 开发者来说,Systrace 最大的作用就是把整个 Android 系统的运行状态,从黑盒变成了白盒。全局性和可视化使得 Systrace 成为 Android 开发者在分析复杂的性能问题的时候的首选。
解析后的 Systrace 由于有大量的系统信息,天然适合分析 Android App 和 Android 系统的性能问题, Android 的 App 开发者、系统开发者、Kernel 开发者都可以使用 Systrace 来分析性能问题。
在遇到上述问题后,可以使用多种方式抓取 Systrace ,将解析后的文件在 Chrome 打开,然后就可以进行分析
使用 Systrace 前,要先了解一下 Systrace 在各个平台上的使用方法,鉴于大家使用Eclipse 和 Android Studio 的居多,所以直接摘抄官网关于这个的使用方法,不过不管是什么工具,流程是一样的:
一般抓到的 Systrace 文件如下
命令行形式比较灵活,速度也比较快,一次性配置好之后,以后再使用的时候就会很快就出结果(强烈推荐)
Systrace 工具在 Android-SDK 目录下的 platform-tools 里面(最新版本的 platform-tools 里面已经移除了 systrace 工具,需要下载老版本的 platform-tools ,33 之前的版本,可以在这里下载:https://androidsdkmanager.azurewebsites.net/Platformtools ),下面是简单的使用方法
1 | cd android-sdk/platform-tools/systrace |
可以在 Bash 中配置好对应的路径和 Alias,使用起来还是很快速的。另外 User 版本所抓的 Systrce 文件所包含的信息,是比 eng 版本或者 Userdebug 版本要少的,建议使用 Userdebug 版本的机器来进行 debug,这样既保证了性能,又能有比较详细的输出结果.
抓取结束后,会生成对应的 Trace.html 文件,注意这个文件只能被 Chrome 打开。关于如何分析 Trace 文件,我们下面的章节会讲。不论使用那种工具,在抓取之前都可以选择参数,下面说一下这些参数的意思:
1 | -a appname enable app-level tracing for a comma separated list of cmdlines; * is a wildcard matching any process |
上面的参数虽然比较多,但使用工具的时候不需考虑这么多,在对应的项目前打钩即可,命令行的时候才会去手动加参数,我们一般会把这个命令配置成 Alias,比如(下面列出的 am,binder_driver 这些,不同的手机、root 和非 root,会有一些不同,可以查看下一节,使用 adb shell atrace –list_categories 来查看你的手机支持的 tag):
1 | alias st-start='python /path-to-sdk/platform-tools/systrace/systrace.py' |
这样在使用的时候,可以直接敲 st-start 即可,当然为了区分和保持各个文件,还需要加上 -o xxx.html .上面的命令和参数不必一次就理解,只需要记住如何简单使用即可,在分析的过程中,这些东西都会慢慢熟悉的。
一般来说比较常用的是
Systrace 默认支持的 TAG,可以通过下面的命令来进行抓取,不同厂商的机器可能有不同的配置,在使用的时候可以根据自己的需求来进行选择和配置,TAG 选的少的话,Trace 文件的体积也会相应的变小,但是抓取的内容也会相应变少。Trace 文件大小会影响其在 Chrome 中打开后的操作性能,所以这个需要自己取舍
以我手上的 Android 12 的机器为例,可以看到这台机器支持下面的 tag(左边是 tag 名,右边是解释)
1 | adb shell atrace --list_categories |
]]>一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 的运行,从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
今天来讲一下为何我们讲到流畅度,要首先说 60 帧。
我们先来理一下基本的概念:
在理解了上面的基本概念之后,我们再回到 Android 这边,为何 Android 现在的渲染机制,是使用 60 fps 作为标准呢?这主要和屏幕的刷新率有关。
目前的情况下
如果要提升,那么软件和硬件需要一起提升,光提升其中一个,是基本没有效果的,比如你屏幕刷新率是 75 hz,软件是 60 fps,每秒软件渲染60次,你刷新 75 次,是没有啥效果的,除了重复帧率费电;同样,如果你屏幕刷新率是 30 hz,软件是 60 fps,那么软件每秒绘制的60次有一半是没有显示就被抛弃了的。
如果你想体验120hz 刷新率的屏幕,建议你试试 ipad pro ,用过之后你会觉得,60 hz 的屏幕确实有改善的空间。
这一篇主要是简单介绍,如果你想更深入的去了解,可以去 Google 一下,另外 Google 出过一个短视频,介绍了 Why 60 fps, 有条件翻墙的同学可以去看看 :
下面这张图是 Android 应用在一帧内所需要完成的任务,后续我们还会详细讲这个:
]]>一个人可以走的更快 , 一群人可以走的更远
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。
本篇文章是一个目录,之后的文章更新后,会在这里进行汇总,内容如下:
目录暂定这么多,后续如果在写作的时候,有更新或者删减,都会在这里进行更新,本篇文章也会置顶一段时间
2023-10 update:
开始准备 Perfetto 系列
本系列的文章既适用于应用开发者,也适用于系统开发者,我会在写作的时候兼顾两者。作为一名系统开发者,我大概理了一下我的优势:
基于上面几点,我在写作的时候也会利用这些优势,给大家带来不太一样的:
计划是每周更新一篇,给自己一个目标 ,给自己一个交代吧。
]]>一个人可以走的更快 , 一群人可以走的更远
首先我们需要知道什么是屏幕刷新率,简单来说,屏幕刷新率是一个硬件的概念,是说屏幕这个硬件刷新画面的频率。
举例来说,60Hz 刷新率意思是:这个屏幕在 1 秒内,会刷新显示内容 60 次;那么对应的,90Hz 是说在 1 秒内刷新显示内容 90 次。至于显示的内容是什么,屏幕这边是不关心的,他只是从规定的地方取需要显示的内容,然后显示到屏幕上。
首先需要说明的是 FPS 是一个软件的概念,与屏幕刷新率这个硬件概念要区分开,FPS 是由软件系统决定的。
FPS 是 Frame per Second 的缩写,意思是每秒产生画面的个数。举例来说,60FPS 指的是每秒产生 60 个画面;90FPS 指的是每秒产生 90 个画面。
VSync 是垂直同期( Vertical Synchronization )的简称。基本的思路是将你的 FPS 和显示器的刷新率同期起来。其目的是避免一种称之为”撕裂”的现象.
对比 60 fps :
60 fps 的系统 , 1s 内需要生成 60 个可供显示的 Frame , 也就是说绘制一帧需要 16.67ms ( 1/60 ) , 才会不掉帧 ( FrameMiss ).
90 fps 的系统 , 1s 内生成 90 个可供显示的 Frame , 也就是说绘制一帧需要 11.11ms ( 1/90 ) , 才不会掉帧 ( FrameMiss ).
Input 的扫描周期在 8 ms左右, 不同的手机会有不同, 由于 Android 系统的 Display 系统是以 Vsync 为基础的, Input 事件也是在 Vsync 到来的时候才会去处理.
所以当一个 Vsync 周期为 16.67ms , Input 周期为 8ms 的时候, 可以保证一个 Vsync 周期内存在 2 个 Input 点.
当一个 Vsync 周期为 11.11ms , Input 周期为 8ms 的时候, 一个 Vsync 周期内可能存在 2 个 Input 点. 也可能存在 1个 Input 点. 这会带来不均匀的 Input 体验.
当屏幕刷新率和系统fps相对应的时候,才会带来最好的效果。从目前的硬件水平来看,90Hz 屏幕 + 90Fps 系统的组合才是目前最好的选择;其他的组合比如 90Hz 屏幕跑 60 fps 的系统,则会有屏幕刷新的时候,系统内容还没有准备好,只能显示上一帧这种情况;比如 60Hz 跑 90 fps 的系统,则会出现丢帧的情况,因为系统内容准备速度要比屏幕刷新速度快,势必有的帧绘制好却没有显示就被丢弃了。
另外 120Hz 的屏幕 + 120 fps 的系统肯定是未来发展的趋势,看看 iPad Pro 就知道了,但是目前的 Android 系统架构和硬件,都不足以支持 120Hz 的屏幕 + 120 fps 的系统,就算强行上,体验也会比较差,而且也会有很多的妥协。
相信到了 2020 年,120Hz 的屏幕 + 120 fps 的 Android 手机大家就可以买到了。
对比视频我们就直接看 OnePlus 的这个视频:90Hz 和 60Hz 屏幕使用对比
简单来说,Android 系统在工作的时候,由下面几个步骤:
需要说明的是,90fps 和 60fps 在流程上基本没有差别。
注: Systrace 是按照时间线从左到右展示
注: Systrace 是按照时间线从左到右展示
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - 新的流畅体验,90Hz 漫谈
]]>一个人可以走的更快 , 一群人可以走的更远
我是一名 Android 系统开发工程师,在某手机厂商的系统研发部门。工作主要是负责 Android 系统相关的工作,比如性能、功耗、稳定性、框架维护等,工作比较杂,都是打杂的工作。
爱好方面,打篮球、跑步、打游戏这些程序员必备技能自然是不能少;读书、养猫、喝茶这些艺术青年的爱好也不落下;业余时间喜欢刷刷 B 站和 YouTube;有空了就把之前落下的没有完成的文章补全;目前沉迷于减肥,正在与小肚子做殊死斗争。
电脑:电脑主要有三台,一台 MacBook Pro 用来码字、写自己的小 App、处理照片等;一台公司配置的台式机,Ubuntu 系统,主要用来工作,编译 Android 源码必备;一台自己配的台式机,主要是用来玩游戏、编译 AOSP 代码。
手机:手机有三台,主力机器是 iPhone X,除了续航其他的都很满意;备用机器是一台 OnePlus 6,Android 旗舰,Android 工程师还是得要有一台使用的 Android 机器的;另外一台是 Pixle,主要是来跑自己编译的 AOSP 代码的,业余学习框架必备。
其他硬件 :Apple 家族的 Watch、iPad Pro、AirPods;索尼大法的无线耳机 MX-1000;Kindle Voyage,看电子书专用。
最理想的工作环境是 Work Life Balance ,就像最近大火的 996 项目里面说的,目前自身的工作状况也差不多,所以我更希望我的工作环境是 955.WLB 这个项目里面描述的,work–life balance。
996 给人最大的压力是没有自己的时间,去提升自己,每天忙到晚上十点十一点,回去洗个澡就睡了,什么健身、读书、学习这些,根本没有时间。正如这篇文章废掉一个人最隐蔽的方式,是让他忙到没时间成长 所说:
如果一个人只是从工作过程中学习,那么这种单一的学习方式,必然导致它对成长的边际贡献越来越低。
每天一定要让自己有时间成长,其实是让你拓展除工作外的其它成长方式,你学习的方式多元化,对成长的边际贡献就越高,你的成长就越快。
一个人在职场里持续上升,必须要有持续的增量成长。
总结来说,我最理想的工作环境:有志同道合的同事、有自己热爱的工作、有自身成长的空间和时间。
一个是阅读,从别人的思考里面提取精华,看看是否可以为我所用或者改进或者发扬光大;一个是思考和记录,边思考边画流程图,记录某个思想的火花。
不过效果都不怎么好,忙起来很少有时间阅读和思考,求各位大神指教。
我最想推荐给大家的不是某个硬件或者软件,而是一种学习方式:记录-总结-输出。这三个环节环环相扣:
记录:我们总说,好记性不如烂笔头,尤其是在现在这个信息爆炸的时代,有自己的一套记录方式显得尤其重要。待办事项、偶尔冒出来的奇怪念头、遇到的问题和解决的方法、在某个网站上看到的好文章、待更新的 Blog、YouTube 上的教学视频等等,记录会让你显得井井有条。
总结:总结可以把之前零散的思路或者知识汇总到一起,清晰的脉络有助于从总体去看某个时期或者某个知识,比如项目总结可以让我们在后续的项目里面避免再走这个项目里面的坑;某个知识点的总结可以让我们从点到面了解这个知识,加深对这个知识的印象。
输出:输出可以是私人笔记、Blog、视频,输出这个更多的是通过向别人讲述来强化自己。为了给别人讲清楚,自己要对输出的点非常熟悉;大家对于输出的讨论也会更加强化你对这个知识点的理解;输出也有助于帮助他人,同时提升自己的名气,你很强,你得让别人也知道你很强。
最近去了趟日本,赶上了樱花季,放一张樱花结束吧。
本文参与了「利器社群计划」,发现更多创造者和他们的工具:http://liqi.io/community/
]]>一个人可以走的更快 , 一群人可以走的更远
有用户反馈,手机在滑动的时候, 列表会一抖一抖的, 滑动桌面或者设置(只要是可以滑动的),都会出现,但是这个并不是必现,而是某些用户会出现,某些用户则不会出现。
吃瓜群众可以直接拉到下面看 罪魁祸首和自检 ,对分析问题比较感兴趣的可以看一下分析的过程。
本地测试有一台复现, 拿过来之后分析发现,手指滑动桌面或者设置,都会必现卡顿, 从 Trace 上看就是下面这样
红色箭头处就是掉帧的地方. 从上面的 Buffer 个数可以看到, SF没有绘制的原因是 Launcher 没有提交 Buffer 上来.
对应的 Launcher Trace如下 , 可以看到 Launcher 没有绘制的原因是没有 Input 事件传上来. 所以 Launcher 的画面没有更新, 所以才会出现掉帧.
没有事件上来这个本身就是有问题的 , 我们手指是连续从屏幕上划过的, 事件的上报应该是连续的才对, 我们怀疑是屏幕报点有问题, 不过 Check 硬件之前我们首先看一下 InputReader 和 InputDispatcher 线程是否正常工作
从图中可以看到 InputReader 线程是正常工作的 , 但是 InputDIspatcher 线程却有问题, 大家可以看一下正常情况下这两个线程的对应关系
再回到有问题的那个图 , 仔细看发现 InputDispatcher 线程的周期是和 Vsync 是相同的, 也就是说, InputDispatcher 的唤醒逻辑由 InputReader 唤醒变为由 Vsync 唤醒
再仔细看的话,点开 InputDIspatcher 的线程 cpu 状态可以看到, 唤醒执行任务的 InputDispatcher 线程并不是被 InputReader 线程唤醒的, 而是被 System_Server 的 UI Thread 唤醒的.
那么接下来, 就需要从代码的角度来看为什么 InputReader 没有唤醒 InputDIspatcher 。
InputReader 唤醒 InputDispatcher 线程的逻辑如下(以本例中的 Move 手势为例。),
frameworks/native/services/inputflinger/InputDispatcher.cpp
1 | void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) { |
需要注意这里 ,mPolicy->filterInputEvent 直接 return了,也就是说这里如果返回 false,那么就直接 return 了, 不继续执行下面的步骤。
继续看 mPolicy->filterInputEvent
frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
1 | bool NativeInputManager::filterInputEvent(const InputEvent* inputEvent, uint32_t policyFlags) { |
这里从 jni 调回到 java 层, 也就是 InputManagerService 的 filterInputEvent 方法。
com/android/server/input/InputManagerService.java
1 | // Native callback. |
跟代码流程发现, 这个 mInputFilter 是 AccessibilityInputFilter 的一个实例, 在 辅助功能里面打开开关的时候,会调用 AccessibilityManagerService 的 updateInputFilter 方法来设置 InputFilter.
android/view/InputFilter.java
1 | final public void filterInputEvent(InputEvent event, int policyFlags) { |
继续看 onInputEvent(event, msg.arg1);
com/android/server/accessibility/AccessibilityInputFilter.java
1 |
|
继续看 processMotionEvent
1 | private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) { |
继续看 batchMotionEvent
1 | private void batchMotionEvent(MotionEvent event, int policyFlags) { |
继续看 scheduleProcessBatchedEvents
1 | private void scheduleProcessBatchedEvents() { |
会在下一个 Vsync 周期的时候执行 mProcessBatchedEventsRunnable , 也就是 Choreographer.CALLBACK_INPUT , 熟悉 Choregrapher 的同学应该知道这里在做什么。
1 | private final Runnable mProcessBatchedEventsRunnable = new Runnable() { |
那么代码在这里就比较清晰了, 是因为存在 AccessibilityInputFilter 导致 InputDIspatcher 线程没有被唤醒,而是把事件处理放到了下一个 Vsync 里面去处理。
结论
问题就在这个 Runnable 里面, 正常情况下, 如果没有打开 AccessibilityInputFilter , 那么上层不会对 Input 事件做任何的拦截, 一旦有 AccessibilityInputFilter , 那么就会走上面的逻辑, 这时候 InputDispatcher 不会跟着 InputReader 的节奏来走 , 而是跟着 Vsync 的节奏来走, 从 Trace 上也可看到这点;
那么这个 AccessibilityInputFilter 是从哪里来的呢?答案就是 Accessibility 服务,也就是常说的无障碍服务。
经过上面的分析我们知道问题的原因是无障碍服务 ,无障碍服务的本质是为了服务哪些盲人之类的不方便操作的用户,但是某些 App 为了实现特定的功能,也加入了自己的 Accessibility 服务, 比如各大手机市场的“一键安装”功能,用户是方便了,但是用不好,也会有负面的作用,比如这一例,导致用户手机整机卡顿,不知道的用户,我估计都要退机了。
那么罪魁祸首是谁呢?目前发现有两个,一个讯飞输入法,一个是应用宝。打开 设置-系统-无障碍服务,可以看到里面的各种软件都有参与到,不过这个默认是关闭的,很多应用会引导用户去开启,许多用户不明所以,就稀里糊涂打开了。
无障碍服务页面如下:
关于无障碍服务有多NB,大家可以自己看看下面的弹框,这东西可以检测你的信用卡号和密码,至于短信内容、微信聊天内容那都是小 Case。
至于在这个例子里面引起整机卡顿的,就是下面这个 监听 ”执行手势“ 这个,一旦有应用监听这个的话, InputDIspatcher 线程就会走 Vsync 的周期,导致报点处理不及时,从而让滑动的对象以为这一帧没有事件进入,所以也没有内容的变更,就不会进行页面的更新,从而导致卡顿。
如果你使用的是 Android 手机,强烈建议你关掉所有的无障碍服务(如果你不需要的话),像自动安装应用这种功能,不值得你为此付出这么大的风险。这个是 Android 原生的问题,我们在 Pixle 和 其他三方手机上都有发现这个问题。
关闭路径:设置-系统-无障碍服务 , 进去后把你已经打开的都关上。
强烈建议 应用宝、讯飞输入法 ,不要监听手势事件。
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 平台应用宝和讯飞输入法无障碍服务导致的全局卡顿分析
]]>一个人可以走的更快 , 一群人可以走的更远
跟 2017年一样,我会把 2018 年里面我觉得体验很棒或者对工作生活很有帮助的东西推荐给大家,或许 2019 你会需要他们。推荐的内容包含了 App、硬件、书籍、博客、专栏等,希望能帮助到看到这篇文章的你
Rescuetime 是一款记录时间花费的软件,它可以准确记录你每一天的时间都花在了哪里(需要安装对应的客户端)
比如你是一个程序员,你想知道自己每天的工作效率,那么使用 Rescuetime 绝对是个好的选择,你可以清晰地看到每天都花了时间在哪里。比如我,每天的大部分时间都是在 Android-Studio、VSCode、Terminator。下面是我1月15号的截图:
编程栏的详细信息
有了上面的数据,你就知道自己每天在电脑上的时间都去哪里了。 这个软件基本上支持所有的平台,尤其是 Linux(不过 iOS上由于系统的原因,只能查看没法记录)。
2018 年我记录每日的工作的软件从 MWeb 换成了石墨文档,很大的原因是石墨文档支持多平台(Mac、iOS、Android、微信小程序),最重要的是 Web 端也非常好用。这样我就可以实时进行记录和查看,分享也非常的方便,可以设置只读和可读可写,多人协作很方便。
另外一个原因是 :一个笔记网页端对程序员最大的尊重,应该是能拦截 和处理 Ctrl+S,印象笔记这个方面做的就很不好,总是出现保存网页,石墨文档则会保存内容。
坚果云是一款提供网盘|云盘|云服务的团队协助软件,可随时随地实现共享文件夹。坚果云网盘支持移动办公,协同办公,文件同步,数据备份,智能管理,在线编辑等功能。
目前在国内支持文件同步的软件,我找来找去,最终还是用了坚果云,很大原因是坚果云支持 Linux,在 Linux 下工作的时候,很多文件直接扔到坚果云的同步文件夹里面,其他各个平台就都有了,突出一个方便。
算是系统开发的经典书了,这书今年出了第二版,加了一些新的内容。不管是应用开发工程师,还是系统开发工程师,多了解 Android 系统的架构和设计,对自己知识的深度是很有帮助的。
不过 Android 的版本发展实在是太快了,阅读此书建议配合最新的 Android 源代码。梳理流程的同时,也要深度思考设计思想。
《深入理解Android内核设计思想》适用于 Android 4.3以上的版本。全书从操作系统的基础知识入手,全面剖析进程/线程、内存管理、Binderv机制、GUIv显示系统、多媒体管理、输入系统等核心技术在 Android 中的实现原理。书中讲述的知识点大部分来源于工程项目研发,因而具有较强的实用性,希望可以让读者“知其然,更知其所以然”。全书分为编译篇、系统原理篇、应用原理篇、系统工具篇共4篇22章,基本涵盖了参与Android开发所需具备的知识,并通过大量图片与实例来引导读者学习,以求尽量在源代码分析外为读者提供更易于理解的思维方式。
《深入理解Android内核设计思想》既适合 Android 系统工程师,也适合于应用开发工程师来阅读提升Android开发能力。读者可以在《深入理解vAndroidv内核设计思想》潜移默化的学习过程中更深刻地理解Android系统,并将所学知识自然地应用到实际开发难题的解决中。
豆瓣 : https://book.douban.com/subject/25921329/
《Android进阶解密》是一本Android进阶书籍,主要针对Android 8.0系统源码并结合应用开发相关知识进行介绍。
《Android进阶解密》共分为17章,从3个方面来组织内容。 第一方面介绍Android应用开发所需要掌握的系统源码知识,第二方面介绍JNI、ClassLoader、Java虚拟机、DVM&ART虚拟机和Hook等技术,第三方面介绍热修复原理、插件化原理、绘制优化和内存优化等与应用开发相关的知识点。3个方面有所关联并形成一个知识体系,从而使Android开发者能通过阅读本书达到融会贯通的目的。
《Android进阶解密》适合有一定基础的Android应用开发工程师、Android系统开发工程师和对Android系统源码感兴趣的读者阅读。
jd : https://item.jd.com/12447229.html
##奔跑吧 Linux 内核
Android 系统工程师必备。
本书内容基于Linux4.x内核,主要选取了Linux内核中比较基本和常用的内存管理、进程管理、并发与同步,以及中断管理这4个内核模块进行讲述。全书共分为6章,依次介绍了ARM体系结构、Linux内存管理、进程调度管理、并发与同步、中断管理、内核调试技巧等内容。本书的每节内容都是一个Linux内核的话题或者技术点,读者可以根据每小节前的问题进行思考,进而围绕问题进行内核源代码的分析。
本书内容丰富,讲解清晰透彻,不仅适合有一定Linux相关基础的人员,包括从事与Linux相关的开发人员、操作系统的研究人员、嵌入式开发人员及Android底层开发人员等学习和使用,而且适合作为对Linux感兴趣的程序员的学习用书,也可以作为大专院校相关专业师生的学习用书和培训学校的教材。
豆瓣 : https://book.douban.com/subject/27108677/
目前出到了第五卷,豆瓣评分 8.3 的国漫,微信读书 5 卷全都有,非常方便看。
《镖人》,一部重现隋唐江湖的热血漫画!
大业三年(公元607年),隋王朝在隋炀帝杨广的残暴统治下民不聊生。身手不凡的镖客刀马行走于西域大漠,在躲避朝廷追杀的途中,他接下了一个目的地为首都长安的护送任务,本以为只是一趟简单的护镖,却没想到一路危机四伏,险象环生。一场牵动天下命运的旅途就此拉开帷幕……
之前读过《大败局1》,被里面的故事深深的吸引,正如豆瓣评论:“三流的文笔,一流的现实。时代的洪流中,企业家、创始人,与赌徒并无区别。野心家迷失于成功的光环,却只能在惨败后看清自己。”
《大败局2》解读九大著名企业盛极而衰的失败原因:“中国第一饮料品牌”是如何陨落的?家电业最具现代气质的公司,是怎样被肢解和蹂躏的?中国民营企业的航母,为何会彻底沉没?股市庄家如何布下资本迷局?最具想象力的汽车革命为什么会一夜流产?房地产最大的黑马失陷何处?最低调的钢铁公司如何迎来最致命的打击?中药业的领头兵因何溃不成军?资本狂人究竟是在点燃全民的热情,还是在玩火自焚?
在《大败局2》中,我们更多地看到了一种“工程师+赌徒”的商业人格模式。他们往往有较好的专业素养,在某些领域有超人的直觉和运营天赋,同时更有着不可遏制的豪情赌性,敢于在机遇降临的那一刻,倾命一搏。这是企业家职业中最惊心动魄的一跳,成者上天堂,败者落地狱,其微妙控制完全取决于天时、地利与人和等因素。
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了 Coding Horror 博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web 设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下。
Jeff Atwood 于 2004 年创办 Coding Horror 博客,记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近 10 万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞。
《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折。
我的一些读书笔记:
今年付费会员里面感觉最值的,就是微信读书的年费会员了。年费会员可以免费看所有的付费出版物,包括听书。所以今年我的大部分的书都是在微信读书里面读的,所以我觉得这个会员办的很值,如果你是一位喜欢读书的人,那么办个会员吧。 我在微信读书读完的书包括
2018 年的新版的 iPad Pro ,不论是在外观、性能、还是配置、价格,都是一个字:NB; 基本上可以拿来当电脑用,不论是看书、看剧、做笔记、看PDF 都非常的舒服,120 HZ 的刷新率简直是一种享受。
如果你计划入手 ipad 的话,强烈建议你入手这款,我自己的 iPad Pro 10.5 那版感觉屏幕有点小了。
《Android 开发高手课》是极客时间推出的专门为 Android 开发者定制的课程,专栏内容包括奔溃、内存、卡顿、启动、IO、存储、网络、耗电、UI、安装包体积等常见的复杂问题的原理分析和借鉴方法,非常值得 Android 开发工程师学习。
微信扫描下面的二维码即可加入学习
《Linux 性能优化实战》是极客时间推出的面向 Linux 和 Android 底层开发者的课程,作者是微软 Azure 资深工程师。专栏中他会以案例驱动的思路,从实际问题触发,带你由浅入深学习一些基本的底层原理,掌握常见的性能指标和工具,学习实际工作中的优化技巧,让你可以准确分析和优化大多数性能问题。
微信扫描下面的二维码即可加入学习
其目录如下:
香帅,真名唐涯,北京大学光华管理学院金融系副教授、博士生导师。香帅承诺用一年的时间,让你透彻掌握金融学的核心知识、全面理解金融学的架构和本质,建立一套完整的金融学思维。内容包好近日世界观、金融机构、工具与市场、投资者决策、公司决策、监管创新与危机、科技金融、金融术与道。
听这门课,记得老老实实做笔记。
其目录如下
]]>一个人可以走的更快 , 一群人可以走的更远
大概的步骤包含下面几个:
建议准备下面的硬件,当然没有也没关系,有了更好
Linux 这边我建议用 Ubuntu 系统,不建议用虚拟机,直接安装一个新的 Ubuntu 系统会比较好,Ubuntu 目前最新的 LTS 版本是 18.04,目前安装 Ubuntu 的步骤会比较简单:
没有 v-p-n 的话,推荐使用清华的镜像站:https://mirror.tuna.tsinghua.edu.cn/help/AOSP/
## repo 下载
1 | mkdir ~/bin |
1 | mkdir WORKING_DIRECTORY |
1 | repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest |
1 | repo sync -c --no-tags |
1 | sudo apt-get update |
1 | sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc unzip |
编译 Android Master 的代码的话,需要下载对应的手机的驱动,在这个页面找到自己需要的驱动:
https://developers.google.cn/android/blobs-preview
extract-qcom-sailfish
解压如下:
在源码根目录下执行
1 | source build/envsetup.sh |
执行下面的命令选择要编译的手机型号和版本(user、userdebug、eng)
1 | lunch |
选择好了之后,输入对应的数字或者数字后面的,执行 make 开始编译(可选择加 -j4,4带代表线程数,机器性能好的话可以8或者16,看cpu):
1 | make -j8 |
out 目录会生成对应的 image
在源码根目录下,执行下面的命令,即可刷入对应的系统到
1 | fastboot flashall |
以下命令都在源码根目录下执行
java 代码推荐使用 AndroidStudio 打开、编辑
c/cpp 代码推荐使用 SourceInsight 、Eclipse、VS Code 打开、编辑
1 | mmm framework/base |
1 | mmm frameworks/base/services |
1 | mmm frameworks/base/core/res |
root && remount 之后,就可以把对应的 framework、Services、Res 等 push 进去,重启 shell 即可生效,或者直接 adb sync system 即可, sync system 会把 out目录下对应机型的 system 目录和手机的 system 目录进行同步,很是方便。
例子:
1 | adb root && adb remount && adb shell stop && adb sync system && adb shell start |
]]>一个人可以走的更快 , 一群人可以走的更远
转自:百度Family,内容为陆奇 2017 年 7 月 的百度内部分享,非常值得技术人员学习和思考
个人非常崇拜陆奇, “陆奇以精力旺盛著称,通常凌晨4点起床,先查邮件,然后在跑步机上跑4英里,边跑边听古典音乐或看新闻。早上5点至6点就办公室,利用这段时间不受别人干扰准备一天的工作,然后一直工作到晚上10点,有时也会在半夜给同事发电子邮件。LinkedIn CEO 杰夫·维纳(Jeff Weiner)曾在雅虎与陆奇共事多年,他开始以为这种日程安排无法持久,后来不得不说:“陆奇的确是我所见过最有干劲的人。”前雅虎工程师阿米特·库玛尔(Amit Kumar)也夸奖陆奇人缘好。陆奇说:“我不觉得累,我热爱每天的工作。”陆奇获有20项美国专利。”
陆奇的演讲核心思想可以归纳为下面五个点,我把它贴在了书房的墙上,时刻提醒自己。
- “我们一定要有一个坚定不移的深刻的理念,相信整个世界终究是为技术所驱动的。”
- “有没有其他人已经解决这个问题?然后你可以把你的时间放在更好的创新上。”
- “做什么事情一定要做最好,一定要是做业界最强的。”
- “我把自己想象是一个软件、一个代码,今天的版本一定要比昨天版本好,明天的版本肯定会比今天好。”
- “看到问题也不要去问别人,就把它 Fix。”
首先要相信技术,我刚才已经讲了,整个我们工业界,特别是像百度这样的公司,对技术坚定的、不动摇的信念特别重要。
我也分享一下,盖茨提到微软公司的宗旨就是:写软件代表的是世界的将来。
为什么?未来任何一个工业都会变成软件工业。盖茨是对的,因为任何工业任何行业自动化的程度会越来越高,最后你所处理的就是信息和知识。
但现在软件的做法又往前提了一次,因为在人工智能时代,不光是写代码,你必须懂算法,懂硬件,懂数据,整个人工智能的开发过程有一个很大程度的提高,但是,技术,特别是我们这个工业所代表的技术一定是将来任何工业的前沿。
所以我们一定要有一个坚定不移的深刻的理念,相信整个世界终究是为技术所驱动的。
我们观察一下,在美国硅谷、在中国,互联网创业公司也好,大型公司也好,大家的起点是越来越高的。为什么现在创新速度那么快?主要是起点高了。我们可以使用的代码模块,使用的服务的能力,都是大大的提升。
在内部我想强调这一点,很多大公司包括微软在内,内部的Code都重做了无数遍。
我现在的要求是,每一次你写一行新的代码,第一要做的,先想一想你这行代码值得不值得写,是不是有人已经做了同样的工作,可能做得比你还好一点。有没有其他人已经解决这个问题,然后你可以把你的时间放在更好的创新上。
特别是大公司里面重复或者是几乎重复的Code实在太多,浪费太多的资源,对每个人的职业生涯都不是好事情。
我再强调,在大公司内部,你写代码之前想一想,你这行代码要不要写,是不是别人已经有了,站在别人的肩膀上去做这件事情。
我要另外强调的一点就是Engineering Excellence,工程的技术的卓越性和能力。
任何市场上竞争就像打仗一样,就看你的部队体能、质量,每一个士兵他的训练的程度,和你给他使机关枪、坦克,还是什么样的武器。
所以Engineering Excellence跟这个类比,我们要建的是一支世界上最强的部队,每一个士兵,每一个领军人,每个人的能力,他的训练都是超强的,然后我们给每个人提供的工具和武器都是一流的。
所以 Engineering Excellence 是一个永无止境的、个人的、团队的,能力的追求和工具平台的创新,综合在一起可以给我们带来的长期的、核心的竞争力,为社会创造价值,最终的目的是给每个用户、每个企业、整个社会创造价值。
我另外还要在这里强调的一点就是 Relentless pursuit of excellence:永无止境的不断的持续的追求。
我们要么不做,要做的事情一定做最好,这是我对大家的要求。数据库也好,做大平台也好,大数据也好,我们要做什么事情,我们一定要下决心,这是我对你们每个人的要求,做什么事情一定要做最好,一定要是做业界最强的。
每天学习,可能是对每个人都是最最重要的。
我今天分享一下,我自己怎么想我自己的。就很简单一个概念,我把自己想象是一个软件、一个代码,今天的版本一定要比昨天版本好,明天的版本肯定会比今天好,因为即使犯了错误,我里面有If statement,说如果见到这个错误,绝对不要再犯。
英语,另外有一句说法就是Life is too short, don’t live the same day twice. 同样一天不要重活两次。每天都是不一样,每天为什么不一样,因为每天都变成最好,每天都变得更好。今天的版本一定要比昨天好,每个好的、杰出的工程师,杰出的技术领袖,一定要保持自己学习的能力,特别是学习的范围。
在这上面我也稍微引申一下,做Computer science的,如果只学Computer science,不去学一些其他的行业,肯定不够。我举个例子,经济学必须要学。为什么这样讲?Computer science它有个很大的限制,他是假定你有输入以后有输出,这种解决问题的方式有它的好处,但有它的限制性。
我给大家举个例子,地图导航,如果你纯粹用这个方式去做,你只是把一个拥挤的地方移到另外一个拥挤的地方。经济学,它对问题的建模是不一样的。它起点是假定是一个整体的一个生态,每个人的输入都是另外一个人的输出,你要用经济学的方式来描述地图导航的问题,你就会去算一个Equilibrium,市场也是这样。
如果把深度学习真的要想彻底,必须把物理重学一遍,把生物学看一遍,再把进化论再看一遍。因为深度学习跟这些东西完全相关,自己肯定想不清楚,要彻底想清楚,必须学。
另外,学产品,我以前跟所有的工程师都讲,如果不懂产品,你不可能成为一个最好的工程师。真正要做世界一流的工程师不光要懂产品,还要懂整个商业,懂生态。因为你的工作的责任,是能够看到将来,把技术展望到将来的需求,把平台、把开发流程、把你的团队为将来做准备。所以学习是非常非常重要的。
最后是从我做起。
我们公司有个非常大的使命,用科技让复杂的世界更简单。整个世界非常非常复杂,人其实所做的事情基本上都是Reduce entropy。
因为从热力学第二定律来讲,世界是会变得越来越乱的,我们想做的事情就是把它变的更简单,让我们生活变得更美好。
而且具体的,我们可以通过人工智能技术来做到唤醒万物,但是这一切是通过每一个人的一点一滴的行为累计起来,从我做起。还有Ownership,看到机会不需要问别人,有机会就去做,看到问题也不要去问别人,就把它Fix。
把我们的使命、把我们的公司当成我们自己每个人的事业来做,我可以坦诚的给每个人讲,如果你把公司的使命,把公司的事业,当成你自己个人的事业,Own everything,你在职业生涯一定是走得最快。从我做起,从身边的每一件事情做起。
]]>我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
一个人可以走的更快 , 一群人可以走的更远
]]>我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
软件开发者身上的所有的“坏毛病”中,最严重的可能是:我们自以为是典型用户。然而大部分的开发者没有意识到的是,我们其实是异类,我们绝非等闲之辈 — 我们是边缘人。
作者提到了“逻辑人”和“现代智人”的概念
逻辑人:逻辑人渴望控制那些让他们感兴趣的东西,而那些让他们感兴趣的都是些复杂的确定性系统。人是复杂的,但他们不像机器,他们的行为不具有逻辑性和可预见性,最好的机器是数字的,因为这样它就能变得最为复杂、精细,病区能被程序员轻易改变。
现代智人:一般的普通用户,他们只是想简单地使用,而不是去控制。
对于逻辑人来说,控制是他们的目标,而复杂是他们愿意为之付出的代价,对于普通人来说,简单是他们的目标,失去控制权是他们愿意付出的代价。逻辑人被一种对工作原理难以抗拒的认知欲望驱使着,相比之下,现代智人渴望的是成功。
Alan Cooper 列了一些逻辑人的典型特征,你可以对号入座一下:
另外一句话软件工程师们需要谨记:任何人都能做出一个没人会用的复杂软件,这其实并不难!把软件做的简单易用才是真本事。你必须停止像逻辑人一样思考,而应该学会像现代智人那样思考。
象牙塔式的开发指的是:开发团队常年封闭在“高塔”之中,一门心思地做着魔法一般的软件。因为缺乏强有力的证据,开发者都假设其他人都是开发者,这是很危险的。
作者建议:在整个项目周期内,请尽力将你的开发人员暴露在用户面前:参加用户会议,参与可用性测试和验收测试,与用户进行交流,分析用户的数据和行为。
Eric 提出的“互信关系”:当人们从你那里购买软件的时候,他们对眼下和将来有很多期望:
优秀的程序员都有自知之明,知道自己能做什么,不能做什么。他们要么直接拷贝别人的优秀设计;要么本分地只做编码,而把界面设计的工作交给其他专家。
是朋友,就别让你的朋友做出只有程序员才会使用的界面。
专家和小白都只是一小部分人,大部分水平相当的用户都属于“中间分子”,你应该重视这些中间分子,中等水平的用户数量是如此巨大,他们如此具有主导性,以至于你可以放弃新手和专家级别的用户。
为了迎合为数不多的新手和专家,你在软件开发过程中耗费了大量的时间,最终只是让产品变得更差,结果还冷落了核心用户群。
用户的愿望与现实几乎总是相悖的,我们提倡要观察用户的实际行为,而不是听他们叙述他们的所作所为,其原因就在于这种背离。观察是一种很强大的技能,要学会通过人民的行为来判断,而不是听他们说什么就是什么。
作者提到了“活跃用户的矛盾体”这个概念:活跃用户的矛盾体是一种自相矛盾,因为如果用户对系统多一些了解,从长远来看,是会节省时间的。但现实世界里,人们的行为模式不是那样的。因此,我们不能忍工程师针对理想化的用户开发产品,因为现实中的人是非理性的,我们必须根据用户的实际行为模式来设计产品。
每个用户都会说谎,预期询问用户是否喜欢你的软件–他们当然会说喜欢,因为当面说你的软件有多么糟糕头型是多么的无理 – 你应该效仿 Gregory House : 去观察他们是否使用了你的软件,以及他们是怎么使用的。基于行为数据去设计你的软件,而不要靠用户说的“谎言”(不管那些谎言带有多大的善意)
衡量程序员是否成功,有个标准是看他发布了多少代码,但是仅仅发布是不够的,有多少用户正在使用你的软件,这才是衡量成功的终极目标
聪明的软件开发者知道,他们的工作远远不止编写代码和发布产品;他们的工作是开发出人民真正想要的使用的软件。这当然包括编码,但还有大量的全局性的其他的事情,比如撰写技术文档、交互设计、培养社区用户、乃至产品愿景,这些对于软件的全面成功都是至关重要的。如果连这一点都没有搞明白,那么你写了什么样的代码就无关紧要了。
用户口述他们想做的事情,与他们实际的所作所为相比,往往天差地别。从可用性的角度来看,询问用户他们想要什么是徒劳的,原因就在这里 – 你必须观察他们正在做了些什么。在可用性方面,你不能猜测行事,你必须去观察用户如何使用你的软件,除此之外别无他法。
在做设计的时候,如果能基于用户对你的软件的实际使用方式来做决定,岂不是更合理?不管你是在“低保真的可用性测试”转几篇每个观察用户,还是收集用户行为数据、然后在无形之中观察用户,宗旨是一样的:别问,须观察。
软件依靠新功能来推动销售,但久而久之,那些新增的功能恰恰是使得软件越变越糟的罪魁祸首 。那种正在慢慢滋生的微妙的“功能癖” — 他会摧毁人们最喜爱的软件。
一个不好的趋势是:软件公司把现有软件修复 bug 的优先级设得比较低,而把为接下来的版本开发新功能这事看的特别重要。导致的结果就是,软件的质量每况愈下。(就像 Flyme 和 MIUI)
我们也许不该在盲目地把软件当成一堆功能来衡量 – 人们总有“食量”限制,就像在吃自助餐时,那么多事物你吃得完么?我们应该以结果为导向,衡量软件在帮助我们完成任务时的生产力或效力。
作者认为,社会工程充其量是一种不精确的科学,即使在网络空间原型里也是这样。有人曾经说过,“在最精心准备的实验中”,即使条件收到最严格的控制,生物也将为所欲为
在构造社会性软件时,人是所有问题的根源,但解决问题最终还得靠那些人
作者总结了自己在 Stack Exchange 的工作内容:我所做的是、我最擅长的是、我最最热爱并且胜过世界上任何其他事情的是,为喜欢相互写几段文字的人们设计大型多人游戏。我吧他们的痴迷,引导到某种积极的事情上面;他们可以从中学习,还可以为整个世界创造一些可以重复利用的美妙作品 — 这依然是我所欲之事,因为我还保留有源源不断的痴迷。
作者提出了 “10个可怕的想法”:
如果你想在网上学点东西,你必须好好设计你的软件,引导人们与生俱来的社会群体冲动,并使他们重新聚焦在有价值的事情上。
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
单元测试真正的价值在于,它迫使你停下来,为测试思考一番。大部分开发人员都不做测试!他们只是随意输入一些数字,然后点几个按钮,如果这个过程中没有发现尚未处理的异常,他们就觉得代码已经足够好了,可以交付给测试团队了。
单元测试让你为刚刚写下的代码思考一连串艰难但又不得不思考的问题:
作者这一节主要举了一个他们实际遇到的例子,由硬件引起的 bug,排查起来异常困难,但如果用对了工具,那么将事半功倍。
尽管软件是不可靠的,但我们不能总把矛头指向软件,有时候,你面对的确确实实是一个硬件问题。
作为一个开发者,你不应该让用户来指出哪里有错误,你应该比用户更加熟悉你的系统。所以你需要建立一种异常和错误报告机制,你需要集中在一个地方去处理所有的错误,这个地方是你团队里面的所有的开发人员非常熟悉的,而且每天会接触到的。比如 Stack Overflow ,用 ELMAH
对于 “测试驱动开发” 的一个思考是时间投入回报比,如果你修复了一个真实用户永远也碰不到的 bug,那么你的修复有什么价值呢?
作者建议大家使用 “异常驱动的开发”:
这种数据驱动的反馈机制是非常有效地,几个迭代下来,你的程序将非常稳定,坚如磐石。
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
作者在做网站的评委的时候,每个参赛的网站只有 30s 的时间,对于此:“在 30 秒内作出评判是完全不公平的,但那也恰恰反映了现实世界中的真实情况”
作者给参赛的提了一些建议,你的网站首页需要给人一种眼前一亮的感觉:
在任何 Web 应用中,设计首页的基本草图是你应该做的第一件事情,因为他是至关重要的初始设计文稿,也是你的远景声明。
追求简单更在于把简单进行到底,从雅虎首页和 Google 首页的历年对比来看,Google 在首页做到了非常克制的简单,而雅虎的首页越来越复杂,信息越来越多,现在来看,这简直就是门户网站的“灾难”
Google 的简单,是把复杂的事情放在了背后,而不是一股脑推给用户
我们应该从简单设计入手,必要时按比例放大,而不是一开始就把事情搞得很复杂,然后被迫收缩,这与目前 Moble First 的设计理念类似。
为什么应用比网站更好?
为什么网站比应用更好?
从上面的对比来看,其实是各有利弊的,不过我们也可以看到后续的发展,网页和 App 会越来越接近,其开发语言、运行环境越来越一致,各种跨平台的框架让 App 和网页的开发不再差异巨大,随着移动互联网的普及,任何 App 和网页都会是以移动设备优先的角度去开发的。
网页和 App 的界限会非常模糊,最后统一。
我们需要采用正确地做事方式,而不是标准的做事方式:
做 Android 系统优化最好也遵循上面的步骤,先弄懂代码逻辑,再弄懂代码为何这么写,然后再去思考如何优化,这期间就需要找到瓶颈,作出修改,拿到用户数据,对比用户数据选择最优解。
iPhone 的单 Home 键设置一直饱受争议,因为这使得后退这个操作比较复杂,在屏幕比较小的机器上还好,可以手势操作,但是在大屏幕机器上,后退到上一页非常不方便,你不得不用另外一个手来进行操作。
如果你的项目里面没人关心可用性,那么记得项目注定会失败。
作者推荐了一本书:《用眼动追踪提升网站可用性》,感兴趣的可以买一本看看
费茨定律: 一个东西越大,离光标越近,它就越容易被点击。
作者概括了一篇“Visualizing Fitts’s Law” 的文章的核心思想:
同理,如果有的必要的按钮你不希望用户点击,那么做小一点总没有错,要让不常用或者危险的 UI 元素难以被点击
网站的写作应该采用“倒金字塔”的风格:在文章的开头先把结论告诉读者,接着再写最重要的辅助信息,最后才介绍相关的背景。
毋庸置疑的是,你应该尽量吧最重要的信息放在顶部,不管你是做一个网页,写一段程序,写一封电子邮件,还是做一份简历,等等
另外作者推荐了一本书 写给程序员的UI设计指南,感兴趣的可以看一下
如果你想再加一个什么 UI 元素,请确信,你所加的那个 UI 元素不是压倒骆驼的最后一根稻草
创新并不是要接受所有的东西,而应该对除了关键性功能之外的所有东西通通说不。
这里主要说的是程序员在做用户界面的时候,总是很粗糙,不易用。不过从现在的发展来看,用户界面设计不再那么困难,各种美观的界面框架可以非常容易的套用。
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
优秀的程序员擅长编程,成为更加优秀的程序员的方法是抛开编程,你必须培养对于编程周边的所有事情的热情
比尔盖茨在2005年的访谈中提到:“工作的本质不是闭门造车,最最匮乏的人才是那些既对工程技术有超强的领悟能力,有可以与核心开发人员建立良好的关系,并且可以充当与客户、市场等之间桥梁的人”
你的兴趣爱好越广泛,就越能胜任你的工作
破窗理论:如果一栋楼的一个窗户破了,并且留在那里不去修复,这栋楼的其他窗户很快就会被破坏。一个长久没有修复的破窗户释放出来的信号是“没人管”,这会让人觉得,即使再破坏更多的窗户也不会付出什么代价
从编程的角度来看,破窗理论也存在,你需要及时采取措施:不要放任“破窗”(不良的设计、错误的决定或者糟糕的代码)不管,一但发现就要尽快修复。如果时间不够,就先把他隔离起来。你可以把这些令人不快的代码注释掉,或者显示“尚未实现”的消息,或者用虚假的数据来代替。
编程是非常注重细节的!如果你不能够掌握这些细节,你就会有一种失控的感觉,而你的项目失控也只是一个时间问题。或许,我们就应该谨小慎微。
我所认识的最杰出的程序员,他们对所从事的事情都有着终生的热忱,他们绝不会因为一次微弱的经济波动而转行去做其他的事情
对于编程:要么热爱,要么离开
在编程开发领域,人们很容易就陷入“越新越好”的思维模式,而忘记了“想法往往比代码更重要”。
Forth 的进化指出了 Charles 在发明和实现 Forth 语言的时候的知道原则:
Charles 认为:简单必须被强制执行,而不是作为一个可有可无的目标。现实中,很多开发者难以保持程序的简单,因为他们没有在需要做艰难决定的时候坚持说“不”,而事事允诺,处处妥协却容易的多
很多人看不上 oppo,但是 oppo 的系统在设计上保持了简单之美,所以 oppo 的系统用起来很轻,很舒服,长时间使用也很少有卡顿的情况出现,低端机的上面的表现也比同类竞品要好很多,这也与 oppo 的开发理念契合:优先保证基础体验(即快省稳)
如果有一段你不再需要的代码,请真正地删除它而不是把它闲置在那里,其主要的原因是为了去除噪音和不确定性,开发者面临的众多最困难的事情之一就是代码流的噪音或者不确定性,因为这会影响他们将来的工作效率。无用的代码会引发其他开发者的思考:
在目前 Git 横行的时代,你更不应该保留那些没用的老代码,如果想看之前的写法,也就是 Git 几行命令的事情。如果你不能有一个好的不删除的理由,那么删掉它就是合理的
这一节提到,并不是所有人都适合编程,事实上,大部分人学不会编程,其中决定因素是他们对无意义事物的态度
形式逻辑证明,进而用一种叫编程语言的形式系统来表达,通过执行某种特别的计算得出结果,这其实是完全没有意义的。为了编写一个计算机程序,你必须做出妥协,赋予程序某种意义,但不管你想要这个程序做什么,计算机都会按照这些没有意义的规则运行,并且得到一些没有意义的结果。在测试中,那些有稳定思维模型的人都体现出了在这方面的先天接受能力,他们都有能力看见规则背后的数学计算问题,而且无论怎样都能遵循那些规则。另外一个方面,那些没用稳定思维模型的人总是找不到头绪。
这节我感觉标题不好,循规蹈矩:原指遵守规矩,不敢违反。现也指拘守旧准则,不敢稍做变动。而文中则说的是你是否遵循基本的规则。
这一节可以理解为:很多事情,包括软件开发,都是有一定的套路或者基本规则的,这些套路或者基本规则是被大家验证过的,你要做一件事的时候,最好按照这些套路或者基本规则来办,比如作者列出了一篇博客中写出更好的代码的 12 个步骤:
虽然上面都是疑问句,但答案是肯定的
科里定律:坚守一个目标。这个定律在现代人居开发的下面几个核心原则中都有体现
科里定律同时也告诉我们,要有意识地选择你的代码不做什么
与你所相信的恰恰相反,单纯地每天埋头于工作并不能算是真正意义上的锻炼 – 参加会议并不能锻炼你的人际交往能力;回复邮件并不能提高你的打字水平。你必须定期流出时间,集中锻炼,这样才能把事情做得更好
上面这个理论也就是我比较推崇的两本书《深度学习》和 《刻意练习》里面所强调的(如果你还没有看过这两本书,那么建议你看一下):一万小时不是一个小时重复一万次,而是每个小时都能全心全意投入学习,深入思考,总结归纳
另外作者也提到了“努力地学习”:要不断地挑战自身能力之外的东西,学习和训练的主要价值在于发现弱点,有针对性地进行提高。“努力地学习”意味着,要常常去处理那些刚好在你能力极限上的问题,也就是那些对你来说有很大可能失败的事情。如果不经历一些失败的话,你可能就不会成长。你必须不断挑战自我,超越自己的极限
作者列举了一些套路(是那些真正可以实施的套路,有些长,不过我还是把所有的条目都摘抄出来,因为我自己也要实施):
另外作者也提到了 Peter Norvig 所列出的一些建议:
最后作者也阐述了自己的编程套路:
当你能编写精彩的代码、并且能用精彩的言辞向世人解释那些代码时,到那时候,我会觉得你已经掌握了最牛的编码套路!
我觉得这一节作者引用的 “Creating My Own Personal Hell” 中阐述独自编程的危害性非常值得深思:
有些人宣称,“独自工作”为建立起自己的工作流程提供了极好的机会。但是,根据我的经验,在团队只有一个人的时候是没有流程可言的。没有任何东西可以帮你抵挡住如潮水般涌来的大量工作。当你的代码太急于求成时,没有人去纠正你的错误。没有人检查你的代码。没有人保证你的代码能准时提交、打好标签、进行常规的单元测试。没有人保证你遵循了某个编码标准。没有人督促你及时修复代码里的缺陷。没有人检验你是否把一个实际存在的问题标注成了“无法重现”。没有人复核你的估算,在你玩忽职守的时候把你抓回来
没有人在你生病时或者出差时接过你的工作。没有人在你工作繁重时帮助你,在你深陷于骚扰电话、无聊会议、还有在最后关头忽然被扔过来(但需要立即解决)的杂碎任务时,没有人能拉你一把。没有人忽然有奇思妙想,帮助你走出困境。没有人在设计、架构或技术上与你合作。你在一个真空中工作;在真空中,没有人能听到你绝望的尖叫
如果你读到了这些内容,请以此为鉴。如果某个公司只招你作为唯一的一位开发者,在你答应他们之前请三思。那根本就是另一种地狱。如果有机会的话,请选择那些能与其他开发者一起工作的职位,这样你至少可以在与别人一起工作的过程中得到指导,这有助于你发展自身的技能,让你在技术方面与时俱进
如果你不能展示给别人看,再漂亮的编码技巧又有什么意义?如果你不去接触其他程序员的不同观点、不同方法以及不同的技术,你又怎么能学到更多的技艺?谁又能检查你的代码并告诉你,那个问题有更简单的解决方法?如果你对待编程的态度是认真的,你应该要求与同伴们一起工作
个人的能力总是有限的,它决定了你在这个领域里只能走那么远。找一些其他的聪明程序员吧,和他们一起工作。努力让自己保持谦逊低调,然后你会很快发现,软件开发其实是一种社会活动——它的社会性比大部分人想象的要大得多。你可以从那些性格内向的同伴身上学到很多东西
就像我们常说的,一个人可以走的更快,但是一群人可以走的更远
在健康的软件工程文化影响下,团队成员通过结对的方式来提高他们的工作质量和生产力,他们明白,他们花在查看同事工作上的时间,总会在别人反过来查看他们自己的交付物的时候得到回报。
作者更是给出了一份数据来说明代码 Review 的作用(有点颠覆我的认知):
在平均缺陷发现率方面,单元测试只能到到 25%,功能测试可以达到 35%,而集成测试也不过45%,相比之下,设计和代码审查的平均功效可以达到 55% 和 60%
编程有很多乐趣,其中一点在于:“你不必独自一人去做。”,所以,谁是你的编程伙伴?
很多公司会给新人配一个导师,通过这种 1V1 的指导,让新人能尽快融入到公司,这与学徒制度很类似
晚上学习理论,白天编程工作 — 这种组合方式特别有效
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
我会把读书过程中一些精彩的言论摘录下来,有时会加入一些自己的见解或者经历,读书笔记的大纲与书本身的大纲是一致的,这也是我从另外一个地方学到并一直在用的“如何阅读一本书”,记录下来方便自己经常查看,也方便读者查看。下面是<程序员的修炼-从优秀到卓越> 读书笔记系列:
作者吐槽说,每天都会有新的 TO-DO 应用诞生,很多人还是管不好自己的时间,TO-DO 列表正在变得吃力不讨好:
作者的建议是,每天早上醒来的时候,如果你不能够用上帝赐予你的大脑使劲想出今天你需要做的最重要的三件事,那么你必须先把这个问题认真解决了。你必须要搞明白,什么东西对于你来说是最重要的,并且能让你充满激情。
工具是浮云,而你的大脑将会伴随你的一生,信任他,训练他。
本节主要探讨了 Google 的 20% 时间理论:在 Google,理论上你可以拿上班时间的 20% 来做任何自己想做的事情。Google 许多伟大的软件都是这 20% 的时间里成型的,比如 Gmail、Google News、Google Talk、AdSense 等。
真正能否在公司执行,需要公司的支持:
在工程师文化主导的公司应该会比较容易实行,“重要的创新和改进可能会在任何时候以自下而上的方式来自于公司的任何人—他们不会按照神奇的总体规划上预定的间隔自己蹦出来。”
本节以《末代独裁》里面的一个例子,来说明说服别人的重要性。如果想落实一件事情,你不能只是把自己的观点表达出来,等待别人的判断,你要说服别人认可你的观点:如果你想要影响别人,你必须有能力说服他们。
对于软件工程师来说,他们所要知道的不仅仅是怎样去写出技惊四座的代码,更重要的是怎样推销他们的想法和产品。
作者做了举了一个说服他人的草根方法:
如果你一味地保持沉默,总是像局外人一样冷眼旁观,你就什么也改变不了。如果你想改变你的工作和生活,你必须学会说服别人。 对于某些人来说(比如我),就常常受困于此,我总结了一下原因,也算是后续的职业生涯需要加强的部分:
当然,打铁还需自身硬,最近我面试有一句话我印象特别深刻:公认的牛人应该是
不要害怕失败,也不要主动寻求失败,失败会自己找上门来。不管你在做什么项目,怀揣着学习和锻炼的态度去完成它,这绝对是值得的,与项目的结果相比,过程才是最大的财富。
如果你没能从一个项目的过程中学到一点东西,这才是真正失败的项目。 所以真正的工作中,项目做完要及时 Review,总结经验和教训,防止下次再踩坑,就算是失败的项目,也有大量的经验在其中。
很喜欢的一句话:“我将会把剩下的5天的课程全都用于让他们变得更加聪明,而不是让他们明白我有多聪明”,不管是做培训还是写博客,要有这样的心态,你不是在炫耀自己多厉害,而是要让看博客或者参加培训的人学到知识,获得真的的成长。
千万不要被身边很多天赋比你高的开发者吓倒,勤能补拙,激情造就天才。
作者以维基百科为例,专家并没有更多的权限,在知识面前,人人平等。有时候反而因为你是专家,而对你的期望或者要求会更多。
古人云:三人行,则必有我师;闻道有先后,术业有专攻,如是而已;无他,唯手熟尔。
作为一名专家,重要的不是告诉别人你知道什么,而是要清楚你应该问什么样的问题,病区灵活运用你所掌握的知识去解决眼下的具体问题,作为专家,你的作用是提供明智的、可执行的方向。
最近面试也有提到专家的问题,作者在这里也介绍了几个阶段,大家可以对号入座一下,记得摒弃自己的领域、专业知识、名声和声誉,然后按照下面的阶段来进行重塑:
本节主要讲的是项目管理相关的,包括个人项目管理和团队项目管理,如果你总是把 “快完成了” 挂在嘴边,那么你需要思考一下自己的项目管理是否有问题,作者建议:鼓励并强制要求程序员创建一张他们所要做的全部事情的列表,然后再为其中的每一项列出子项,并且尽可能把所有的子项都加进来
下面是具体可能的措施,把他用在项目里面吧
信任可以解决大型软件管理的问题,但是信任无法代替管理,两者相辅相成。
博伊德迭代法则:迭代的速度胜过迭代的质量。也就是大家常说的,天下武功唯快不破。
Gmail 的成功之路是漫长的,很多人一开始并不看好他,但是在产品发布之后,用户的反馈反而很好。
一夜成名的传说容易让人误入歧途,并且遗毒不浅,如果你打算做一个全新的东西,要有打持久战的准备。
作者认为,成功需要付出多年的努力,你必须踏踏实实地在这件事上花费几年的时机去磨练,每天一醒来就开始工作,日复一日地坚持,不断取得反馈,每一天都比过去做的更好。即使你偶尔会不开心,甚至失去了乐趣,单这些都是为了获取成功所必须的。
作者同时也举了写博客的例子,日复一日的投入,作者花了3年,才让他的博客在行业有了一席之地。在目前这个浮躁的环境下,能静下心来,持续做知识输出的人,我相信一定会有好的结果,只要你相信:你正在做的事情是真正值得去做的。
《程序员的修炼——从优秀到卓越》是《高效能程序员的修炼》的姊妹篇,包含了Coding Horror博客中的精华文章。全书分为8章,涵盖了时间管理、编程方法、Web设计、测试、用户需求、互联网、游戏编程以及技术阅读等方面的话题。作者选取的话题,无一不是程序员职业生涯中的痛点。很多文章在博客和网络上的点击率和回帖率居高不下—— from 豆瓣
Jeff Atwood于2004年创办Coding Horror博客(.codinghorror.),记录其在软件开发经历中的所思所想、点点滴滴。时至今日,该博客每天都有近10万人次的访问量,读者纷纷参与评论,各种观点与智慧在那里不断激情碰撞 —— from 豆瓣
]]>《程序员的修炼——从优秀到卓越》的写作风格风趣幽默,且充满理解和关怀;适合从新手到老手的各个阶段的程序员阅读,也适合即将成为程序员的计算机和相关专业的学生阅读。《程序员的修炼——从优秀到卓越》能够帮助读者更多地关注技术工作的人性和人文因素,从而实现程序员职业生涯的成功转折 —— from 豆瓣
知乎上有一个问题,一个用户问 “Android 系统不释放内存吗?”,用户并不是不知道系统会释放内存,而是想知道其中的细节,好优化使用的体验,下面我就从用户的几个问题入手,来简单说明一下,比较深入的细节我后续的文章会详细介绍。
同时也会对第一段中提到的几个问题提出一些个人的见解,欢迎一起来讨论,留下你的问题,提出你的见解,大家共同进步。
这个是不准确的,只能说对了一半. 你所描述的”android系统下关闭程序”,指的是怎么个关闭法呢?目前阶段有好几种关闭程序的方法:
这种退出的方法, 进程是否被杀掉,取决于这个应用程序的实现. 举个栗子,如果你创建一个空的应用, 这时候查看系统内存信息(包名为com.exmaple.gaojianwu.myapplication,pid为5708,内存为13910kb):
可以看到,这个应用程序的pid为5708 , 其优先级为Foreground,即前台程序.
这时候我们点击Back键退出,然后再查看系统的内存信息(adb shell dumpsys meminfo)
我们看到,这个程序在 Back 键之后,其进程 5708 依旧是存在的.只是其进程优先级变成了Cache.其占用内存变成了 12337kb,和之前的 13910kb 相比是变小了一些. 但是大部分内存是没有被释放掉的.
在任务管理器中杀掉应用,这个结果是不一致的,其取决于这个OS的任务管理器的实现,大部分国内的厂家都会对任务管理器进行定制,以达到更有效的杀掉应用的效果.一般来说厂家定制的任务管理器都会比较暴力,除了少数白名单,其他的应用一概直接将进程杀掉
我们以上面的那个测试程序为例,打开这个程序之后, 其进程优先级为Foreground,这时候我们直接调用任务管理器杀掉改程序(以魅族MX4 Pro为栗子):
可以看到用任务管理器杀掉之后, 整个应用程序的进程都被杀掉了.
我们可以通过 adb shell am force-stop 包名来杀掉这个程序,其结果也是进程直接被杀掉,IDE(比如Android Studio)选择一个进程后,点击图中的按钮
也是可以干掉这个进程的. 这时候进程是被直接杀掉的。
这个不对,一个进程被杀死后,其内存会被释放掉的,不管是虚拟机内存还是 Native 内存还是图像所申请的内存,都会被系统回收,放到可用内存中。
我们以知乎 App Android客户端为栗子,打开这个程序之前,系统剩余内存 1.8G:
打开知乎这个程序之后,系统剩余内存:
知乎占用的内存:
使用任务管理器杀掉知乎(直接杀掉进程),系统剩余内存:
可以看到,杀掉进程之后,系统可用内存是会增加的。
这个和第一条一样,取决于你关掉进程的方法:
从上面的三个逻辑来看,用户最佳的使用方法应该遵循下面的建议:
上面也讲到,国内的手机厂商,都会禁止应用被杀后走自启动模式,或者被其他进程唤醒,一般有名单进行控制,这样做是为什么呢?厂商这么做无非是为了限制应用对资源的占用,一个应用退到后台之后,系统肯定是希望其占用最少的资源,能不占资源最好(cpu、gpu、io、memory),但是国内的部分应用却没那么安分守己,大家肯定听说过全家桶和相互唤醒,应用被杀了没问题,我另外一个应用偷偷把你拉起来就可以了,这拉起来的过程就占用了系统的资源,可能会导致前台应用出现卡顿,或者导致整机内存不足,所以厂商对应用的限制是无可厚非的。
那么应用为什么要抱团取暖,相互唤醒呢?第四个问题就是说这个的。
这句话没毛病,但是是有前提的,前提就是,如果这个应用在后台可以安分守己,至于现实嘛..不说大家也知道,参考百度全家桶
Android设计的时候,确实是想让大家不去关心内存问题,Android会有一套自己的内存管理机制,在内存不足的时候通过优先级干掉一些应用。每个应用在接收到内存不足的信号,需要根据内存不足的程度,来释放掉一部分内存.以保持自己的进程不被杀死,这样下次启动的时候就不用去fork zygote,这样的话,下一次启动的时间确实会少很多,也就是大家常说的冷启动和热启动的差距。
但是……………..凡是总有个但是,理想是丰满的,现实是很残酷的。严格按照Google想的那一套去做的应用不多,国内开发者对内存的敏感程度很低,导致很多应用程序跑起来分分钟就100-200MB 了,墨迹天气这样的应用,400m 妥妥的(不好意思又黑了墨迹天气) 。所以手机低内存的情况非常常见,所以低内存的情况会很频繁。这时候你再起一个应用,申请内存的时候发现内存不够,就开始杀应用了。
所以经常会出现你在看电子书,突然这时候微信来了个消息,你切过去回了个消息,打开相机拍了个照,然后发给朋友,又发了条微博,再回来看书的时候发现电子书已经挂了,正在重新加载程序….WLGQ…
这时候你就发现限制后台进程的重要性了,把不重要的进程直接干掉,限制应用的自启动和相互唤醒,保证重要的进程不会被系统杀掉,也就保证了用户的基本使用体验。
所以说不重要的程序是需要在使用结束后直接干掉的.一劳永逸,麻麻再也不用担心这货偷跑流量/后台安装程序/占内存/占 CPU 了….
再说后半句: 可以减少启动的时间. 这个是对的, 如果一个应用程序的进程没有被杀死,那么下一次启动这个应用程序的时候,就不需要去创建这个进程了(fork zygote,这个耗时还是蛮多的), 而是直接在这个进程中创建对应的组件即可(Android四大组件),速度比冷启动要快很多。
以汽车发动为例:
这里来简单解答一下第一段中提到的那些问题
]]>一个人可以走的更快 , 一群人可以走的更远
由于 Google 对 Android 持开放态度,各个手机厂商生产不同产品定位的机器,以及各个 Android 应用的质量良莠不齐,导致影响 Android 流畅度的因素非常非常多,并非大家简单的以为是系统没有优化好,很多时候你会发现,不同 SOC 但是相同的系统,体验完全就是两种。
所以我想和大家聊聊影响 Android 系统流畅性的一些原因,后续大家遇到卡顿的问题,也不会单纯把锅甩给系统,或许你卸载一个 App 就解决了呢.
我想从下面几个方面展开聊这个话题:
- 硬件层面
- 系统层面
- 应用层面
- 流畅度优化闭环
准备好了,那就开始吧,欢迎你加入讨论
cpu 是手机硬件里面最核心的一个器件,这也是把 cpu 作为第一个来说的主要原因,cpu 之所以重要,是因为 Android 系统的运行过程中,大部分是跟 cpu 打交道,cpu 的能力强弱,直接决定了这款手机的档次。
手机 cpu 目前主要有高通、华为、三星、联发科四家在做,每家都有高中低档,高端 cpu 的排名大概是 高通>华为>三星>联发科, 具体的排名可以去这里看(仅供参考):http://www.mydrivers.com/zhuanti/tianti/01/
各个厂商提供的 SOC 里面,通常包含了 cpu 和 gpu ,所以大部分情况下,只要一说 cpu,其 gpu 也是对应确定的,比如高通骁龙845 SOC 带的 gpu 就是 Adreno 630。
gpu 的能力强弱更多的影响的是 gpu 强相关的应用和游戏,比如绝地求生-刺激战场 、 崩坏3 、极品飞车、狂野飙车等。反而王者荣耀这样的游戏更多的是吃 cpu 而不是 gpu。
随着 Android 版本的更新,以及硬件的更新换代,Android 系统对内存的需求越来越强,目前4G内存的手机基本上已经成了标配,旗舰机器没个 6G 或者 8G 你都不好意思说自己是旗舰。
内存主要影响系统行为,内存越大,系统就越可以以空间换时间:后台可以缓存更多的进程,杀进程不再那么激进;可以根据用户习惯预加载一些文件或者进程;各种虚拟机、hwui、进程的参数可以往宽松里调。反馈到用户那里,就是快。
当然如果后台进程多了又没有管住在后台跑,那么又会很耗电,有点得不偿失,这也是为什么国内的系统都会对进程管理这一块进行魔改。
ufs 和 emmc 都是面向移动端的 flash 标准,我们最长听说的就是 emmc5.1 和 ufs 2.1 ,具体可以参考这篇文章:https://zhuanlan.zhihu.com/p/26431201
对于用户来说,ufs 和 emmc 的差异主要在文件读取速度、视频加载速度、文件拷贝等方面,总之能上ufs就别考虑 emmc。
不过有时候这个也是需要 SOC 支持的,比如高通 660,就不支持 ufs,能不买就别买吧。
我们最常见的屏幕分辨率是 1080P,即 1920*1080. 对用户来说,屏幕分辨率除了会影响视觉感官外,还会在系统某些地方有差异,比如截图、录屏、合成等操作。越高的屏幕分辨率,在这里的耗时就越久,也越耗电。
这也是部分 2K 手机在某些场景下,把分辨率降低到 1080P 去运行的原因。比较失败的一个例子就是当年的魅族 MX4 Pro ,在硬件性能不足以支撑 2K 的情况下,强行上了 2K 屏幕,导致很多情况下,用户反馈又卡又耗电。
电池大小决定着续航,也决定着手机设计,手机厂家往往需要在这两者之间找一个平衡,在电池技术没有突破的情况下,就算各家都有快充,还是建议用户在选购手机的时候,尽量选大容量电池的手机,比如 Oppo Find X 或者 Vivo NEX,或者华为 Mate 10.
SoC的全称叫做:System-on-a-Chip,除了我们之前说的 cpu、gpu,Soc 上还有很多器件,具体可以看这篇文章:https://zhuanlan.zhihu.com/p/37634251
,这里就不展开讲了。
SoC 是整个手机最重要的部分,是一切体验的基础。现在高通、三星、MTK 给手机厂家提供的硬件就是 Soc ,以及其配套的 Android 适配系统。手机厂商拿到这个之后,在其基础上做整机的设计,系统这边会在配套的 Android 适配系统上做移植,也就是把各家系统差异化的东西移植到新系统上。
从目前的高通、三星、MTK 三家的适配系统的质量来看,高通提供的适配系统是功能最完善的,高通在 AOSP 的基础上,加上了高通自己的非常多的优化代码,并提供了完善的参数供手机厂商去配置,总的来说开发起来是很舒服的,本身系统的问题不会太多,加上高通的文档完善,支持速度快,国内那么多手机厂商都在用高通也就不足为奇了。
至于专利费,该给的要给啊。
我们经常会说,如果魅族早点用高通的 Soc,早 TM 上市了。
大部分 Android 应用开发者对国内的手机厂商恨的咬牙切齿,最大的原因就是国内系统对应用管控这一块进行了大量的魔改,除非你是 QQ 或者微信,否则灭了屏结果都一样。
国内厂商这么做,不是没有原因的,国内应用厂商的全家桶相互唤醒,已经到了一种丧心病狂的地步,牵一发而动全身,一点都不夸张。
我们遇到的很多用户反馈的整机卡顿问题,抓 Trace 和 Log 来看,都是后台有应用在乱跑,或者后台大量的进程常驻,内存根本不够,而这些普通用户根本就不知道怎么去处理。
所以国内厂商一般会在系统里面做限制,以保障用户的基础体验:
另外手机厂商会有其他的逻辑清理后台的应用,尽管你是合理存在的。
对进程的严格管控,也导致了国内系统的体验有一定的影响,首当其冲的是通知,如果一个应用没有接入这个手机厂商提供的 push sdk,那么他这辈子别想发通知给用户了,如果接入了手机厂商提供的 sdk(目前大部分应用的普遍做法),由于应用不在后台,用户点击通知要等好久才可以进入到对应的界面,毫无用户体验可言。
手机厂商常常会根据手机的内存大小来定制各种不同的策略,比如后台应用的缓存个数、LowMemoryKiller 的阈值、杀进程模块的阈值、显示模块的缓存大小阈值、用户最常用应用的个数等。
很多低端机用户反馈卡顿,我们查看发现,内存是造成卡顿的主要元凶,在低内存的机器上,由于内存不足,系统会频繁杀后台,同时也有频繁的内存->文件,文件->内存 的操作,Trace 上很多 BlockIO,很多平时执行很快的操作,现在执行要很久,再加上部分进程被杀之后马上重启,重启之后又被杀,cpu 占用很高,此时就会很卡。
随着 Android 系统和应用的更新,只会越来越吃内存,目前4G内存是标配,明年或许 6G 才是标配了,能上 8G 尽量上 8G。
进程调度策略有时候也会影响用户的流畅性,当应用的渲染链路上,有哪个环节因为某些原因,没有被调度到的时候,很大可能会造成卡顿。
调度不到在 Trace 上的表现是 Runnable,常见的调度不到的情况有:
另外由于 cpu 引起的卡顿情况还有:
系统调优往往需要针对上面的情况做对应的处理,给用户一个好的用户体验。具体的调优方式,往往跟系统和 Soc 强相关,又涉及到 Kernel 和 功耗,改起来是牵一发而动全身,需要非常谨慎。
Android 应用的渲染链路上最重要的就是主线程和渲染线程,主线程就是应用启动时创建的 MainThread,对应的也会创建一个 RenderThread(硬件加速默认开启),我们平时比较看重的 GPU Profile 那条线,基本就包含了主线程和渲染线程的各个阶段的执行时间,从 GPU Profile ,就可以很容易看到应用的瓶颈
大部分应用的卡顿都发生在主线程和渲染线程上,比如:
很多编程的不好的实现,都可以在上面几个步骤里面体现出来,这些都可以通过 Systrace 看出来。
当前应用的渲染链路上的一切优先级都应该是最高的,后台的进程不应该对其造成影响,这也是系统优化的核心要素,不过要做到这一点也是比较难的,你很难考虑到所有的情况,比如有的用户的使用环境就是很复杂,而且都是必须的,这时候就不是很好处理。
之前有提到 TripleBuffer,这个是 Project Butter 引进的,其中 Vsyncv 和 TripleBuffer 的引进使得 Android 的流畅度上了一个台阶,关于这个可以参考这篇文章 : https://niorgai.github.io/2017/03/21/Android-Draw-System/
对于用户来说这个是透明的,影响的是 GPU Profile 的展示,有时候如果有一条线超过 16 ms 的警戒线,它不一定代表着卡顿,这就是 TrileBuffer 的作用。
后续我会有文章专门讲这个,如何判断是真正的卡顿。
对用户来说,Art 虚拟机相比 Dalvik 虚拟机,最大的提升就是解放了应用的主线程,主线程不再频繁被 GC 线程 Stop ,相应卡顿也减少了很多。
当然 Art 带来的好处不止这一点,Art 随着几个大版本的缝缝补补,已经在很多地方远远超过了 Dalvik,有兴趣的可以自己查一下。
之前提到,一旦触发温控或者低电量,系统会对资源做一定的限制,防止手机无限制过热或者快速关机。这限制就包括
总之,这些限制或多或少会对用户造成影响,最大的影响就是卡顿,这就是很多人会遇到打游戏的时候突然很卡的一个原因。
所以说选购手机的时候,除了要看 Soc,还要看散热是否做的够好,电池是否做的够大。
复杂的布局往往是应用卡顿的最主要的元凶之一,复杂的布局意味着更长的 Measure、Layout、Draw ,这会拖慢主线程的执行速度
ListView、RecyclerView 的新的 Item 在初始化的时候也会有类似的问题,由于此时一般是在滑动,这时候的卡顿感会更明显,用户也更容易察觉,这个从 Trace 上也很容易看出来。
过多业务逻辑导致的卡顿和响应慢的问题,拿淘宝来举例子最合适不过了,每次你冷启动淘宝的时候,进入主界面马上滑动,总感觉跟吃了屎一样,点按钮点不动,滑界面滑不动,虽然最近的版本有优化,不过你找个低端 Android 机,还是原来的配方。
淘宝在启动的时候,需要动态加载很多东西,导致主界面响应很慢,很多东西要动态加载完成后才可以操作,后台还有大量的 dex2oat 操作,可以说是很忙了。
频繁申请和释放内存,会导致内存颠簸,从 AS 的内存监视器可以看到这一点,短时间内内存曲线上下跳动非常频繁,这时候你需要检查一下是否代码写的有问题。
慢网络指的是用户请求网络耗时很久,这会导致用户在某些界面等待内容需要很久,比如知乎经常会出现这种情况,在用户看来,这就是卡了。
设计和性能往往不可兼得,需要从两者之间做取舍,设计师的设计往往很炫酷,互相嵌套的动画往往是程序员的噩梦,为了实现这些复杂的效果,程序员往往需要复杂的代码来实现,这对应用的渲染链路的压力是非常大的,而且在不同性能的机器上表现差异很大,高端机用户觉得这个效果棒棒哒,低端机用户卡的要骂娘。
程序员需要有这方面的知识和数据,好与设计师动之以情晓之以理。
不过用户是很挑剔的,现在的用户对性能的要求越来越高,哪怕是低端机用户,所以合适的设计应该考虑到这部分用户、或者针对低端机用户做区分。
俗称 bug ,很多程序员不喜欢解决性能问题,因为这个东西解决起来,性价比很低,拿我司的程序员来说,解一个性能问题的时间,可以解决十几个界面显示的问题,还未必能真的解决。
不过由于代码实现错误引起的性能问题,必须要最高优先级解决。
开发阶段就用各项数据来做监控和对比,尽量模拟用户的使用环境,尽早暴漏性能问题,早日解决。
在用户使用阶段,收集性能数据,针对这些数据做分析,找出用户最多遇到的性能问题,针对性地做优化。需要注意此时不能在用户阶段手机太多的信息,否则会导致观察者效应
至于需要收集的数据,则需要根据相关度模型来做判断,卡顿发生的时候,系统的哪些指标是可能导致卡顿的原因,那么这些指标就是我们收集的数据。
另外用户的场景判断也非常重要,需要知道用户是在哪个场景出现的卡顿,一旦用户的数量到了一定的级别,这个是很容易发现问题的。
大数据发现问题后,后续就是针对性地进行优化,把用户最常遇到性能问题的场景进行排序,对最常见的场景进行调研和优化。很多时候需要与应用开发厂商进行沟通,
然后需要把这些场景纳入到实验室监控环境里,做到 实验室监控 —> 模拟用户 — > 大数据收集 —> 针对性优化 —> 实验室监控补充. 这样一个闭环。
]]>一个人可以走的更快 , 一群人可以走的更远
今天我们要说的,就是 iOS 和 Android 系统差异化的一个重要体现:StartingWindow,通俗点说,就是应用启动页。iOS 和 Android 都有 StartingWindow,但是表现却完全不一样。iOS 开发要求应用必须要有一个 StartingWindow,且必须是一张图片(iOS 开发可以说说是否支持定制 Layout),用户点击桌面图标启动应用,不会有任何延迟,会立即显示这个图片;Android 系统的 StartingWindow 虽然也是系统提供的,但是由于开放性,Android 允许开发者自己定制 StartingWindow、disable StartingWindow、透明化 StartingWindow。
由于 StartingWindow 对用户体验非常重要,但是应用开发者对 StartingWindow 的滥用,导致了用户启动应用的时候,会获得非常不好的体验,我举个例子你们就明白了:
大家可以安装一下我上面说的那些个应用自己体验一下,我是真的没法体会为何会有那么多应用采用第三种方式,偏偏很多用户只有一台手机,不认为是应用的问题,反而觉得是系统比较慢,然后各种投诉到系统这边,还是四五星的 Bug,真的是欲哭无泪。
我建议采用了第三种方式的应用的开发者,面壁思过一下,你们的产品经理是不合格的,你们作为开发没有主动提出优化这个也是失职的。不用怀疑微信也是他们的一员,只不过微信作为超级 App,在很多系统里面不杀他而已,不信你 adb shell am force-stop com.tencent.mm 一下试试。
不过我今天不想说微信,我想说说知乎,因为我本人是知乎的重度用户,所以最近被知乎的体验搞得心情极差,再加上 N 多用户投诉启动慢,我必须来说两句了。
最近知乎安卓版的版本(5.17.2),虽然没有采用第三种方式,采用的是第一种方式,但是用户体验极差,具体说就是:点击桌面图标启动应用 -> 显示白屏(取决于你手机的性能,在1-5s 之间)-> 显示知乎是广告页 -> 显示知乎首页。我们之前说,白屏是大部分系统的默认 StartingWindow ,之所以会出现这样的现象是因为知乎那令人发指的应用启动速度。
我并不想从代码级别去分析他都干了啥,因为很多人对这个不感兴趣,感兴趣的可以自己抓 MethodTrace 看一下就可以了,这里我们简单从 Systrace 来看一眼就可以了,修复 Bug 的事情,让知乎的程序员去干吧。
从 StartingWindow(白屏) 出现到应用显示第一帧(SurfaceFlinger 视角,数据来自 Mix2s)
从 StartingWindow(白屏) 出现到应用显示第一帧(应用进程视角,数据来自 Mix2S)
Log 视角(看 am_activity_launch_time :2684)(数据来自 Pro7,Mix2s 是1552)
1 | 05-21 22:40:58.033 1252 3371 I am_create_activity: |
这长达 1.9s 的时间,用户看到的都是白屏,更不用说后面还有 3s 的广告页,然后才到主界面,说好的用户体验呢。。。而且我用的机器是小米 Mix2S,高通845加持,那些低端机器就更不用说了,时间只会多不会少。
用户看到的就是下面三个步骤(截图来自 Pro7)
首先点击桌面后,显示 StartingWindow(白屏页面)
2s 后,白屏页面跳转广告页面:
3S 后跳转知乎主界面
我建议知乎还是采用大多数 Android 应用目前采用的第二种方法,即定制简单的 StartingWindow,用户体验好不说,也能拉近 iOS 和 Android 版本的体验差距,比如下图(截取自知乎 iOS 版本)
很大程度上来说,Android 系统的用户体验取决于第三方应用,各个厂商定制的系统能提升一部分,但是一旦进入了应用的界面,体验就完全取决于应用了。
StartingWindow 是一个很小的点,但毕竟是应用的入口,入口做的都不好的话,怎么能吸引用户每次都去访问你呢?知乎只是我举的一个小例子,毕竟还有非常恶劣的采用第三种方式的应用垫底,希望通过这个例子,各位应用开发者能体验到应用启动速度对用户的直接影响,俗话说,天下武功,唯快不破,不是没有道理的。应用优化的好,系统自然也会沾光,最终受益的还是用户。
最后还是挂一下那些非常影响用户体验的应用(采用了第三种方法),也希望读者可以反馈,我会持续更新,他们是
]]>一个人可以走的更快 , 一群人可以走的更远
做了这么久性能相关的工作,也接触了不少模块,说实话要做好性能这一块,真心不容易。为什么这么说? 是因为需要接触的知识实在是太多了,Android 是一个整体,牵一发而动全身,不是说只懂一个模块就可以做好。
在学习的过程中,除了看源码,我还接触到了很多互联网上已有的知识,各位前辈们将他们的知识和经验倾囊相授,让我少走了很多弯路。我在自己的笔记里面存了很多很优秀的技术文章和技术文档,现在我决定将这些放到网上,让每一个想进入 Android 系统开发和优化这个领域的人,能通过阅读这篇文章,快速入门。同时也算是我对知识的一个梳理,查漏补缺,终身学习。
这篇文章记录了 Android 性能优化所必须掌握的知识,涵盖性能优化相关的方方面面(当然如果读者同学你也有很棒的私藏文章,也可以加入到这篇文章里面)。部分文章可能需要特殊的技巧才能看到,希望你已经掌握了这一部分技巧。另外附送 Android 开发者学习路线(2020 版本)。
这篇文章会持续更新,最新更新时间:2022-06-27。
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
知乎 - Android 性能优化必知必会
掘金 - Android 性能优化必知必会
]]>一个人可以走的更快 , 一群人可以走的更远
2017 既然才刚刚过去,我觉得有必要把 2017 年里面我觉得体验很棒或者对工作生活很有帮助的东西推荐给大家,或许 2018 你会需要他们。推荐的内容包含了 App、硬件、书籍、器材等,需要说明的是,这些东西是我觉得 2017 年给我带来很大帮助的,适合我不一定适合你。话不多说,直接上内容吧!
关键词 :减肥 学习 见识
如果你是一个健身爱好者、计划减肥者,想找一款能记录运动的 App,那么我推荐 Keep 给你。Keep 那句很出名的 slogan 不知道大家有没有听说过:自律给我自由!,我同样喜欢另外一句:不要让工作毁掉你的生活,总有比你更忙的人在运动。
软件只是一个辅助,最重要的是要自律,生活需要自律,运动需要自律,饮食需要自律。自律并不意味着生活会失去乐趣,相反,自律给我自由和自信,人总是向往完美的自己,Keep It!
2017 在 Keep 的帮助下,我的体重从最重时候的 78 kg 到了现在的 70 kg,可能数值上没有什么大不了的,但是回过头来看照片对比,现在真的要自信很多。
正如我们家领导所说,有些人你知道他很厉害,仅此而已。
得到这个 App 确给了我们一个途径,近距离地接近那些很厉害的人,知道他们的学习方式、思考方式。你会发现,比你聪明的人比你还要努力,你还有什么理由去偷懒呢?
2017 我听了吴军的《硅谷来信》,听了薛兆丰的《北大经济学课》,听了万维钢老师的《精英日课》,听老罗唠叨了一年的《逻辑思维》,听了许多书。
从长见识和思考两个方面来说,我获得的就已经远远超出这些专栏的订阅价了。
读书的平台很多,选中一个坚持下去是最重要的,微信读书、多看阅读、Kindle 三个平台我都有读书,没有谁好谁坏,重要的是真正获取到了知识。
现在的低头族被大家吐槽,与其在地铁上刷微博、知乎、即刻,不如打开一本书,静下心来好好阅读。
关键词 :工作安排 工作记录
这东西价格是贵了一些,但是是真的好用,各种功能都很齐全,各个平台版本(iPhone、iPad、Mac)数据也都是互通的.
Things 主要完成我工作三环中的第一环:任务安排。 很多时候会有很多任务,有时候会忘记做一些重要的事情,现在我会把所有要做的事情都记录在 Things 里面,每天早上会根据优先级,筛选出今天要做的事情。如果有其他突发的事情,也会记录下来,这样不会顾此失彼。
另外 Things 的重复任务、任务优先级、任务时间等都比较好用,之前用网页端 Tower 的我果断吧数据都迁移到 Things 了。
Mweb 是一款 MarkDown 软件,我也是换了好几个才换到这个,功能齐全,界面美观,MarkDown 写起来很舒服,对图片的支持也很很。
MWeb 主要完成我工作三环中的第二环:任务记录,包括每日的任务完成情况记录。Things 用来安排任务,但是任务完成后要做记录和总结,这就是每日任务记录,Things 记录的是流水,而 MWeb 则记录的是思想。
另外我还使用 MWeb 来写 Blog、公司文档、总结等,言而总之,就是一款好用的 MarkDown 软件。
印象笔记就不用多介绍了,其强大的浏览器剪切插件,帮助我存储了不少非常好的技术文章;团队共享功能,帮助我在团队中分享笔记。
印象笔记 主要完成我工作三环中的第三环:存档。不管是技术文章,还是会议纪要,还是项目规划,还是 PDF,如果要找一个长期存放的地方,那么印象笔记是一个相当可靠的软件;有时候开会不做 PPT,直接用印象笔记的演示功能也可以胜任。
关键词 :墙
作为一个资深的 Google 全家桶爱好者,影梭是 Android 完整体验的强有力的一个保证;作为一个 Android 开发者,我没法想象如果没了影梭,我会失去多少获取知识的渠道。
爬梯需谨慎,感谢国家还没有赶尽杀绝,大家别问我是怎么搞的,低调才能生存。
国内的 Android App 的流氓程度大家是有目共睹的,尤其是 BAT 全家桶,管不好这些应用,Android 的体验会大打折扣,尽管现在很多厂商都针对流氓软件做了限制,绿色守护仍然是你拿到新机后必须要安装的一个软件。
我有幸跟绿色守护作者冯老师在微信群里讨论过问题,冯老师对技术的深度和热情,我等实在是望尘莫及。我们都欠冯老师一个捐赠版。
再也不用嫉妒朋友的iPhone手机,即使安装大量应用,也不会变得迟缓和耗电。有了『绿色守护』,你的Android设备也能永葆第一天拥有它时的爽滑持久!
『知乎』上用户认同度最高的Android省电软件:http://www.zhihu.com/question/21007772
『绿色守护』帮助你甄别那些对系统全局性能和耗电量有不良影响的应用程序,并通过独特的『绿色化』技术,阻止它们消耗您的电池电量,占用您的宝贵内存。经过『绿色化』工艺处理的应用,在您没有主动启动它们的时候,无法『偷偷』运行,而在您正常启动它们时仍然拥有完整的功能和体验,正如iPhone应用那样!
关键词 :自我提升提升
强烈推荐此书,尤其是需要终生学习的程序员们,可能每个人读完后的感想都会不一样,但是我绝对是受到了里面一些做事方法的启发,结合自身的能力和工作,针对性地进行提高,而且得到了较好的效果。个人认为是我 2017 年读的最好的书。在这个碎片化严重的互联网时代,快知识的消费和满足,让自己产生了惰性,认为自己知道的东西很多,其实没有经过深加工,吸收的那些知识只是碎片而已,随着时间就消逝了。
随时随地收发电子邮件、一个接一个地参加大小会议、在即时通讯软件的尖叫中手忙脚乱、在繁杂的多线程工作中不断地切换注意力……你看起来非常忙碌,甚至在不自觉地享受这种忙碌,但你的忙碌真的能转化为生产能力吗?
本书作者、麻省理工学院计算机博士卡尔·纽波特,尖锐地道破了信息经济时代的惊人真相——知识工作者60%以上的工作时间都花费在处理此类浮浅事务上,而这些工作不仅产出的价值有限,还会永久性地损害人们深度工作的能力!
作者创立的“深度工作”概念,其含义为在无干扰的状态下进行专注的职业活动,使个人的认知能力达到极限。而正因为当前社会深度工作能力的日益稀缺,其与经济成功的关系也变得日渐紧密起来。本书的所有讨论也围绕 “深度工作”而展开,全书共分为两部分:在第一部分中,作者从神经科学、心理学、哲学等角度,客观地分析了在新经济形势下实现深度工作的重要性。第二部分则系统地传授了在日常生活中践行深度工作的具体策略,如将深度工作纳入日常工作进程、提高大脑的深度思维能力、远离社交网络等。
作者还强调,深度工作不是一项过时的技能,而是将人们从技术垄断导致的精神异化状态中解救出来的良药。在当前这个以网络为中心的浮浅信息时代,倡导深度工作无异于呼唤一种匠人精神的回归。
豆瓣:https://book.douban.com/subject/27056409/
这本书,也算是声名在外。其实这本书告诉我们一个很简单的道理,想要成为任何一个行业的专家,你都需要进行大量的刻意练习,而不是单纯的堆积时间。
对于在任何行业或领域中希望提升自己的每个人,刻意练习是黄金标准,是迄今为止发现的最强大的学习方法。
豆瓣:https://book.douban.com/subject/26895993/
关键词 :
说来惭愧,2017年没有认认真真读完一本技术书,但从我阅读过的那么多技术书中,还是有两本可以推荐给大家,不过我阅读的技术书,跟我自己的工作相关的比较多,不一定适合你。
算是系统开发的经典书了,这书今年出了第二版,加了一些新的内容。不管是应用开发工程师,还是系统开发工程师,多了解 Android 系统的架构和设计,对自己知识的深度是很有帮助的。
不过 Android 的版本发展实在是太快了,阅读此书建议配合最新的 Android 源代码。梳理流程的同时,也要深度思考设计思想。
《深入理解Android内核设计思想》适用于 Android 4.3以上的版本。全书从操作系统的基础知识入手,全面剖析进程/线程、内存管理、Binderv机制、GUIv显示系统、多媒体管理、输入系统等核心技术在 Android 中的实现原理。书中讲述的知识点大部分来源于工程项目研发,因而具有较强的实用性,希望可以让读者“知其然,更知其所以然”。全书分为编译篇、系统原理篇、应用原理篇、系统工具篇共4篇22章,基本涵盖了参与Android开发所需具备的知识,并通过大量图片与实例来引导读者学习,以求尽量在源代码分析外为读者提供更易于理解的思维方式。
《深入理解Android内核设计思想》既适合 Android 系统工程师,也适合于应用开发工程师来阅读提升Android开发能力。读者可以在《深入理解vAndroidv内核设计思想》潜移默化的学习过程中更深刻地理解Android系统,并将所学知识自然地应用到实际开发难题的解决中。
豆瓣 : https://book.douban.com/subject/25921329/
这本书我还在读,由于缺乏相关的知识,所以进度有点慢。Android 系统工程师必备。
本书内容基于Linux4.x内核,主要选取了Linux内核中比较基本和常用的内存管理、进程管理、并发与同步,以及中断管理这4个内核模块进行讲述。全书共分为6章,依次介绍了ARM体系结构、Linux内存管理、进程调度管理、并发与同步、中断管理、内核调试技巧等内容。本书的每节内容都是一个Linux内核的话题或者技术点,读者可以根据每小节前的问题进行思考,进而围绕问题进行内核源代码的分析。
本书内容丰富,讲解清晰透彻,不仅适合有一定Linux相关基础的人员,包括从事与Linux相关的开发人员、操作系统的研究人员、嵌入式开发人员及Android底层开发人员等学习和使用,而且适合作为对Linux感兴趣的程序员的学习用书,也可以作为大专院校相关专业师生的学习用书和培训学校的教材。
豆瓣 : https://book.douban.com/subject/27108677/
关键词 :旗舰
并不是我不想推荐 iPhone X,是因为买不起……
尽管魅族的 Pro7 Plus 被网友各种吐槽,但你不能否认这是一款非常优秀的旗舰机,舒心的 flyme 系统加上不错的硬件搭配,创新的小窗逼格满满,配不配得上 Pro + Plus 的称号,仁者见仁智者见智吧。
关键词 :美剧
小米路由器+小米家庭影院,最大的好处是方便。现在有很多美剧网站,资源下载都会有个下载到小米路由器的链接,点击就可以下载到自家的路由器上,回家就可以看,突出一个方便。
关键词 :完美
在我眼里,iPad Pro 既具有 iPhone 的优点,又具有 Mac 的优点。从使用场景来看,开会、收发邮件、看电子书、看技术文档、看 PDF等,都可以胜任。
iPad Pro 搭配键盘(别买笔,没用),完全就是一个缩小版本的 Mac,除了写代码,其他的都可以满足我的需求,有了这货之后,我那用了好几年快退役的 Mac 使用率更低了。
前面我提到的那些软件:Keep、得到、微信读书、Things、MWeb、Evernote 都有 iPad 版本,提要要比 iOS 版本好太多。
苹果出品,必出精品,两个字送给 iPad Pro :完美!
关键词 :记录
iWatch 是苹果四件套里面存在感最弱的,个人认为最大的问题是续航,一代一天一冲,二代两天一冲,实在是有点无力吐槽。主力机从 iOS 切换到 Android 后,电话短信闹钟提醒这几个功能完全没用了,不过这货解决了一个我的痛点:运动记录。
跑步机 + iWatch,简直是减肥利器,事无巨细的运动记录也 push 我不要偷懒,言而总之,iWatch 是我生活中不可或缺的。
关键词 :降噪
在嘈杂的办公室,拥有一个带降噪的无线耳机真的是非常重要。降噪能给你一个安静的环境,不管是思考还是写代码还是看书,不受外界打扰的感觉真的很棒;无线带来简洁,没了线的束缚会方便很多,也会少了凳子绕线的苦恼。
MDR-1000X 我用了这么久,降噪、易用性、音质这三个方面都很满意。据说戴耳机会降低 50% 的被打扰率,推荐你也入手一个降噪耳机。
关键词 :坚持
人总是会有惰性,大家都知道跑步减肥,但是就是坚持不下来,而且外出跑步受环境的影响太大,太冷太热下雨下雪。
我在本来就拥挤的家里强行放了一台跑步机,跑步机的好处是,随时都可以跑,而且对跑步的抗拒心理没那么严重。每天下班后,打开 Keep,开跑,每天五公里并不难,重要的是要坚持。
当然还有一些能显著提高幸福感的东西,就不一一列举了,我想起来就会更新到下面
]]>一个人可以走的更快 , 一群人可以走的更远
这博客从我毕业开始写东西,写写删删,也算是记录了一些东西,自己的工作内容也从 App 开发换到了系统 App 开发,在换到系统开发,也算是走了一圈,一些路程,记录下来,几年后再看看,怀念一下也是不错的。
这篇文章我记录了自己的博客、自己的工作、自己的工作内容、自己的工作习惯、还有对 2017 年的期望,有迷茫,也有奋斗。
17年已经过了 30% 了,希望还不算晚。
看到上一篇博客文章的更新时间,已经是去年的这个月了,想想还真是惭愧,每次想动笔重新开始写一些东西的时候,总是由于这样或者那样的原因,没有动手去写。
记得刚毕业的时候,我是很喜欢写东西的,学习笔记也有,工具教程也有,什么都敢写,什么都敢往博客上面放,后来随着工作的深入,懂的东西变多之后,写东西反而不是很多了,我想一个原因是,随着技术水平的慢慢提高,我意识到自己的技术深度还远远不够,很多东西自己都知其然不知其所以然,这样的状态,写出来的东西,会不会误导人呢?
不过,现在技术有了一点点沉淀之后,我觉得有必要把自己总结的一些经验和技巧分享给大家,另外一方面也算是一个记录。很多思路和想法,如果不去记录下来,很容易就会忘记,
从 HTC 的实习,到火花乐蛙的短暂停留,再到魅族,算起来工作了也好几年了,这几年一直在做 Android 相关的开发,从 App 开发到系统开发,一路下来,越发觉得,做软件开发是一件很有趣的事,尤其是当你做的功能,被几百万几千万的用户在使用的时候,那份激动和责任,总会驱动我们要做得更好,我推测未来五年之内, Android 在手机界的统治地位,暂时没有其他的系统可以撼动,所以值得花时间在这上面。
当然除了具体的 Android 技术点,学习 Android 的设计思想,软件架构,培养自己解决通用问题的能力,是更重要的事情,这正是我现阶段需要去努力的方向。
关于具体的技术田,我暂时还没有想好具体要深入哪一块,目前对 Android 和 Linux 的进程管理和 CPU 调度比较感兴趣,性能方面则偏重于流畅度、响应速度的分析和调优,不得不说这几块就够我钻研好久了。
前几天去总部,看到 2016 年我司出了 15 台手机,每一台手机在做的时候,我们都会对其做性能调优,那一大堆性能 Feature ,和每个机型都莫名其妙的性能问题,搞得团队根本没有时间去做一些更有深度的事情,有点故步自封的感觉,2017 年希望能改变这一点,起码自己要先做改变。
工作内容主要是系统层面的一些优化工作,涉及到的点比较多,自己则是全而不精,今年要寻找一个点深挖:
2016 年起,我养成了一套自己的工作习惯,不过有的时候都没有严格去执行,2017 年则需要严格去执行这些,
每天早上起来后,会安排一下今天一天的工作,安排的依据是昨天的工作记录情况和邮件记录,主要包含下面几点
当然会预留一定的时间,去应付突发的事情,比如有人来找我分析很重要的问题,就会打乱我的计划,所以工作安排也是一门技术,以我的经验和公司的情况,我一般会如下安排时间:
当然理想是丰满的,现实是骨干的,鉴于软件开发的不确定性,上面的安排也经常会失效,有人建议用桌子上放一个番茄钟的方式来避免别人的干扰,我个人的经验是,戴个耳机!
我安排自己工作的软件是 Tower , Tower 本来是一个团队软件,不过我们团队不是很适应这个软件,所以在我安利了一段时间后,他们就放弃了,所以我还是自己一个人用, 优先级和时间都会以标签的方式显示在每个任务之前:
每日回家之后,会把每天的工作记录下来,Tower 适合安排工作,但是不太适合记录工作,一是不太方便每日查看,二是自己的一些思路和想法,记录到 Tower 上很容易找不到。
所以我记录工作的软件是 MWeb ,会记录每天所完成的各个项,不论大小,比较重要的工作项,解题思路和想法都会记录在后面,每周的总结也会记录,来源包括 Redmine、邮件、Tower 等,这样不会漏掉一些重要的事情和数据
文章记录主要的印象笔记,遇到好文章或者比较重要的事情,我都会记录到印象笔记中,定时去整理和查看
今年公司比较动荡,身边好几个小伙伴都走人了,公司也在转型,阵痛期。自己也比较犹豫,不过目前没有花太多的时间去想这事,做好目前手上每一件事,该做的去做,机会总是青睐有准备的人。
作为一个软件工程师,Coding 能力永远是要放在第一位的,这一点需要向我偶像百万学习(下图,一周Coding 的时间是51个小时)!
一个人可以走的更快 , 一群人可以走的更远
]]>本文是 Android Bottom navigation 的第二篇文章,主要介绍样式、行为与规格。
Because bottom navigation actions are presented as icons, they should be used for content that can be suitably communicated with icons.
由于底部导航操作显示为图标,它应该使用与其内容相符合的图标。根据以下条件来为每个操作设定样式:
Tint the current bottom navigation action (including the icon and any text label present) with the app’s primary color
用应用的主色调给底部导航操作(包括图标与当前标签文字)上色。
如果底部导航条已着色,将底部导航操作图标和文字设置为白色或黑色。
文本标签为导航图标提供简明的定义。应避免使用较长的文本而造成文本被裁截或遮挡。
点击底部导航图标将直接跳转至相关的界面或刷新当前的界面。
每一个底部导航图标都必须指向一个目的,不应打开主菜单或跳转至其他窗口。
每一个底部导航图标都会随着界面的滚动而动态的显示或隐藏。
在内容区域使用滑动手势不能进行界面的跳转。
在当前界面与未激活界面的跳转过程中使用淡入淡出的动画效果。
用底部导航栏的总长度除以图标的个数,计算出每个图标的宽度。也就是说,要使得每个底部导航图标占有最充足的空间。
宽度的最大及最小值(这些数据包含边距):
高度:
56dp
图标:
24*24dp
内容对齐:
文本与图标需居中且水平。
边距:
文本标签:
用底部导航栏的总长度除以图标的个数,计算出每个图标的宽度。
宽度的最大及最小值(这些数据包含边距):
当前界面
未激活界面
高度:
56dp
图标:
24*24 dp
内容对齐:
文本与图标需居中且水平。
边距:
文本标签:
常规 Roboto 字体: 14sp(当前界面)
由于 snackbars 的层级高度(elevation) 为6dp,而 navigation bar 的层级高度为 8dp,所以 snackbars 显示在 navigation bar 的后面。而 Bottom sheets, navigation drawers 和 keyboards 都显示在 navigation bar 的前面,完全覆盖 navigation bar 。
]]>一个人可以走的更快 , 一群人可以走的更远
本文是 Android Bottom navigation 的第一篇文章,主要介绍 Bottom navigation 的使用,以及 Bottom navigation 小变迁。
上上一次 Bottom navigation 在 Android 圈引起轩然大波还是微信的改版(5.2版本),在试错之后迅速换回了 Bottom navigation 。
而上一次 Bottom navigation 在 Android 圈引起轩然大波的就是 Google Plus 的改版:
反正打脸啪啪啪,不过规则是用来遵守的,你看微信没有遵守,活的比 Google + 好几百倍了。
闲话说完了我们就来看看官方发布的 Bottom navigation 的设计规范,毕竟对于我们广大程序员来说,设计方面的能力还是没有专业的设计师强的,有一些规范我们做出来的东西不至于太难看。
Bottom navigation 主要为手机应用设计,它提供了应用内顶层视图的快速导航功能。一些大的显示设备,比如桌面设备,可以使用侧面导航达到类似的效果。
Bottom navigation 的使用时机:
当 Bottom navigation 与 tabs 混合使用的时候,可能会造成使用上的混乱。 举个栗子,点击 tab 和点击 Bottom navigation 可以显示不同的内容组合,这些功能的组合会给用户带来使用上的混乱。
另外 Tabs 在体验上和 Bottom navigation 也有很大的不同,Tabs 更依赖手势操作带来的便利,而 Bottom navigation 则由于位置比较靠近手指,使得点击更加方便。
]]>一个人可以走的更快 , 一群人可以走的更远
利用python或者直接用adb命令怎么计算apk的启动时间呢?就是计算从点击图标到apk完全启动所花费的时间。比如,对游戏来说就是点击游戏图标到进入到登录界面的这段时间。
已知的两种方法貌似可以获取,但是感觉结果不准确:一种是,adb shell am start -w packagename/activity,这个可以得到两个值,ThisTime和TotalTime,不知道两个有什么区别,而且与实际启动时间不匹配,两者相加都可能比实际启动时间小(测试游戏的时候差别更大);另外一种是通过adb logcat的方式,感觉获取的结果也与实际有差别。
我和另外一个同事郭启发 针对两个方面进行了回答,不过毕竟知乎上看的人会比较少,所以我在征得他的同意之后,将这两个答案整理了一下,记录到博客中,一来算是一个小的总结,之后自己看得时候比较方便,二来给需要的同学一个更加方便的途径。
事实上 Android 中一个 App 的启动时间可以准确计算的.但是要分场景.也就是说要分开游戏和应用. 大家都知道,在Android中,游戏开发和应用开发是两码事.所以我们需要分开来说.
我们平时在写应用的时候,一般会指定一个 mainActivity ,用户在桌面上点击这个 Activity 的时候,系统会直接起这个 Activity. 我们知道 Activity 在启动的时候会走 onCreate/onStart/onResume .这几个回调函数.
许多书里讲过,当执行完 onResume 函数之后,应用就显示出来了…其实这是一种不准确的说法,因为从系统层面来看,一个 Activity 走完 onCreate/onStart/onResume 这几个生命周期之后,只是完成了应用自身的一些配置,比如 window 的一些属性的设置/ View 树的建立(只是建立,并没有显示,也就是说只是调用了 inflate 而已) . 后面 ViewRootImpl 还会调用两次performTraversals ,初始化 Egl 以及 measure/layout/draw. 等.
所以我们定义一个 Android 应用的启动时间, 肯定不能在 Activity 的回调函数上下手.而是以用户在手机屏幕上看到你在 onCreate 的 setContentView 中设置的 layout 完全显示为准,也就是我们常说的应用第一帧.
上面扯得有点远,不感兴趣的话可以不看,下面直接说方法.
题主说的 adb shell am start -w packagename/activity,是可以完全应用的启动时间的.不过也要分场景.
也就是我们常说的冷启动,这时候你的应用程序的进程是没有创建的. 这也是大部分应用的使用场景.用户在桌面上点击你应用的 icon 之后,首先要创建进程,然后才启动 MainActivity.
这时候adb shell am start -w packagename/MainActivity 返回的结果,就是标准的应用程序的启动时间(注意 Android 5.0 之前的手机是没有 WaitTime 这个值的):
1 | ➜ adb shell am start -W com.media.painter/com.media.painter.PainterMainActivity |
总共返回了三个结果,我们以 WaitTime 为准.
关于ThisTime/TotalTime/WaitTime的区别,下面是其解释:
“adb shell am start -W ”的实现在 frameworks\base\cmds\am\src\com\android\commands\am\Am.java 文件中。其实就是跨Binder调用ActivityManagerService.startActivityAndWait() 接口(后面将ActivityManagerService简称为AMS),这个接口返回的结果包含上面打印的ThisTime、TotalTime时间.
ThisTime、TotalTime 的计算在 frameworks\base\services\core\java\com\android\server\am\ActivityRecord.java 文件的 reportLaunchTimeLocked() 函数中。
我们来解释下代码里curTime、displayStartTime、mLaunchStartTime三个时间变量.
正常情况下点击桌面图标只启动一个有界面的 Activity,此时 displayStartTime 与mLaunchStartTime 便指向同一时间点,此时 ThisTime=TotalTime。另一种情况是点击桌面图标应用会先启动一个无界面的 Activity 做逻辑处理,接着又启动一个有界面的Activity,在这种启动一连串 Activity 的情况下(知乎的启动就是属于这种情况),displayStartTime 便指向最后一个 Activity 的开始启动时间点,mLaunchStartTime 指向第一个无界面Activity的开始启动时间点,此时 ThisTime!=TotalTime。这两种情况如下图:
在上面的图中,我用①②③分别标注了三个时间段,在这三个时间段内分别干了什么事呢?
看到这里应该清楚 ThisTime、TotalTime、WaitTime 三个时间的关系了吧:
一个应用 Activity pause 的耗时。也就是说,开发者一般只要关心 TotalTime 即可,这个时间才是自己应用真正启动的耗时。
Event log中 TAG=am_activity_launch_time 中的两个值分表表示 ThisTime、TotalTime,跟通过 “adb shell am start -W ” 得到的值是一致的。
最后再说下系统根据什么来判断应用启动结束。我们知道应用启动包括进程启动、走 Activity生命周期 onCreate/onResume 等。在第一次 onResume 时添加窗口到WMS中,然后measure/layout/draw,窗口绘制完成后通知 WMS,WMS 在合适的时机控制界面开始显示(夹杂了界面切换动画逻辑)。记住是窗口界面显示出来后,WMS 才调用reportLaunchTimeLocked() 通知 AMS Activity 启动完成。
最后总结一下,如果只关心某个应用自身启动耗时,参考TotalTime;如果关心系统启动应用耗时,参考WaitTime;如果关心应用有界面Activity启动耗时,参考ThisTime。
如果是你按Back键,并没有将应用进程杀掉的话,那么执行上述命令就会快一些,因为不用创建进程了,只需要启动一个Activity即可。这也就是我们说的应用热启动。
游戏启动的话,就不适用用命令行的方法来启动了,因为从用户点击桌面图标到登录界面,既有系统的部分也有游戏自己的部分。
游戏也有一个 Activity,所以启动的时候还是会去启动这个 Activity,所以系统启动部分也就是用户点击桌面桌面响应到这个Activity启动。
一般游戏的主 Activity 启动后,还会做一些比较耗时的事情,这时候你看到的界面是不能操作的,比如:加载游戏数据、联网更新数据、读取和更新配置文件、游戏引擎初始化等操作。从游戏开发的角度来看,到了真正用户能操作的界面才算是一个游戏真正加载完成的时间。
那么这个时间,就得使用 Log 来记录了,因为加载游戏数据、联网更新数据、读取和更新配置文件、游戏引擎初始化这些操作,都是游戏自己的逻辑,与系统无关,所以得由游戏自己定义加载完成的点。
对于游戏的启动时间,我们更倾向于计算从点击桌面图标到用户可以与游戏进行交互这个时间段作为一个游戏的启动时间。
计算机最让人着迷的一点就是其准确性,1+1 永远等于 2,启动耗时多久就是多久,每一次可能不一样,但每一次的时间都是这一次的准确时间。
不过每个公司由于对应用的定位不同,所以对应用启动的要求也不一样。比如有的做 ROM 的公司,其内置应用的启动时间一定是要非常快的,这样给用户的第一感觉就是快、流畅;互联网公司的 App 则不是很关心启动速度,大部分互联网公司的应用都有一个启动页,用来展示广告或者功能介绍之类的,然后才会进入到主界面。需求不一样,这么做也无可厚非,不过从消费者的角度来看,越早见到主界面当然越好。
所以在做一个 Android App 的时候,一定要记得将应用的启动时间作为一个性能指标,毕竟:
天下武功,唯快不破!
]]>一个人可以走的更快 , 一群人可以走的更远
上一篇中我们最终使用的 DelayLoad 的核心方法是在 Activity 的 onCreate 函数中加入下面的方法 :
1 | getWindow().getDecorView().post(new Runnable() { |
我们一一来看涉及到的类和方法
Activity 的 getWindow 方法获取到的是一个 PhoneWindow 对象:
1 | public Window getWindow() { |
这个 mWindow 就是一个 PhoneWindow 对象,其初始化的时机为这个 Activity attach 的时候:
1 | final void attach(Context context, ActivityThread aThread, |
这里需要注意 Activity 的 attach 方法很早就会调用的,是要早于 Activity 的 onCreate 方法的。
上面我们说到 DecorView是 PhoneWindow 的一个内部类,其定义如下:
1 | private final class DecorView extends FrameLayout implements RootViewSurfaceTaker |
那么 DecorView 是什么时候初始化的呢?DecorView 是在 Activity 的父类的 onCreate 方法中被初始化的,比如我例子中的 MainActivity 是继承自 android.support.v7.app.AppCompatActivity ,当我们调用 MainActivity 的 super.onCreate(savedInstanceState); 的时候,就会调用下面的
1 | protected void onCreate( { Bundle savedInstanceState) |
由于我们导入的是 support.v7 包里面的AppCompatActivity, getDelegate() 得到的就是AppCompatDelegateImplV7 ,其 onCreate 方法如下:
1 | public void onCreate(Bundle savedInstanceState) { |
就是这里的 mWindow.getDecorView() ,对 DecorView 进行了实例化:
1 | public final View getDecorView() { |
第一次调用 getDecorView 的时候,会进入 installDecor 方法,这个方法对 DecorView 进行了一系列的初始化 ,其中比较重要的几个方法有:generateDecor / generateLayout 等,generateLayout 会从当前的 Activity 的 Theme 提取相关的属性,设置给 Window,同时还会初始化一个 startingView,添加到 DecorView上,也就是我们所说的 startingWindow。
当我们调用 DecorView 的 Post 的时候,其实最终会调用 View 的 Post ,因为 DecorView 最终是继承 View 的:
1 | public boolean post(Runnable action) { |
注意这里的 mAttachInfo ,我们调用 post 是在 Activity 的 onCreate 中调用的,那么此时 mAttachInfo 是否为空呢?答案是 mAttachInfo 此时为空。
这里有一个点就是 Activity 的各个回调函数都是干嘛的?是不是平时自己写应用的时候,貌似在 onCreate 里面搞定一切就OK了, onResume ? onStart?没怎么涉及到嘛,其实不然。
onCreate 顾名思义就是 Create ,我们在前面看到,Activity 的 onCreate 函数做了很多初始化的操作,包括 PhoneWindow/DecorView/StartingView/setContentView等,但是 onCreate 只是初始化了这些对象.
真正要设置为显示则在 Resume 的时候,不过这些对开发者是透明了,具体可以看 ActivityThread 的 handleResumeActivity 函数,handleResumeActivity 中除了调用 Activity 的 onResume 回调之外,还初始化了几个比较重要的类:ViewRootImpl / ThreadedRenderer。
ActivityThread.handleResumeActivity:
1 | if (r.window == null && !a.mFinished && willBeVisible) { |
主要是 wm.addView(decor, l); 这句,将 decorView 与 WindowManagerImpl联系起来,这句最终会调用到 WindowManagerGlobal 的 addView 函数,
1 | public void addView(View view, ViewGroup.LayoutParams params, |
我们知道 ViewRootImpl 是 View 系统的一个核心类,其定义如下:
1 | public final class ViewRootImpl implements ViewParent, |
ViewRootImpl 初始化的时候会对 AttachInfo 进行初始化,这就是为什么之前的在 onCreate 的时候 attachInfo 为空。ViewRootImpl 里面有很多我们比较熟悉也非常重要的方法,比如 performTraversals / performLayout / performMeasure / performDraw / draw 等。
我们继续 addView 中的root.setView(view, wparams, panelParentView); 传入的 view 为 decorView,root 为 ViewRootImpl ,这个函数中将 ViewRootImpl 的mView 变量 设置为传入的view,也就是 decorView。
这样来看,ViewRootImpl 与 DecorView 的关系我们也清楚了。
扯了一圈,我们再回到大标题的 Post 函数上,前面有说这个 Post 走的是 View 的Post 函数,由于 在 onCreate 的时候 attachInfo 为空,所以会走下面的分支:ViewRootImpl.getRunQueue().post(action);
注意这里的 getRunQueue 得到的并不是 Looper 里面的那个 MessageQueue,而是由 ViewRootImpl 维持的一个 RunQueue 对象,其核心为一个 ArrayList :
1 | private final ArrayList<HandlerAction> mActions = new ArrayList<HandlerAction>(); |
当我们执行了 Post 之后 ,其实只是把 Runnable 封装成一个 HandlerAction 对象存入到 ArrayList 中,当执行到 executeActions 方法的时候,将存在这里的 HandlerAction 再通过 executeActions 方法传入的 Handler 对象重新进行 Post。
那么 executeActions 方法是什么时候执行的呢?传入的 Handler 又是哪个 Handler 呢?
我们之前讲过,ViewRootImpl 的 performTraversals 方法是一个很核心的方法,每一帧绘制都会走一遍,调用各种 measure / layout / draw 等 ,最终将要显示的数据交给 hwui 去进行绘制。
我们上一节讲到的 executeActions ,就是在 performTraversals 中执行的:
1 | // Execute enqueued actions on every traversal in case a detached view enqueued an action |
可以看到这里传入的 Handler 是 mAttachInfo.mHandler ,上一节讲到 mAttachInfo 是在 ViewRootImpl 初始化的时候一起初始化的:
1 | mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this); |
这里的 mHandler 是一个 ViewRootHandler 对象:
1 | final class ViewRootHandler extends Handler{ |
我们注意到 ViewRootHandler 在创建的时候并没有传入一个 Looper 对象,这意味着此 ViewRootHandler 的 Looper 就是 mainLooper。
这下我们就清楚了,我们在 onCreate 中 Post 的 runnable 对象,最终还是在第一个 performTraversals 方法执行的时候,加入到了 MainLooper 的 MessageQueue 里面了。
绕了一圈终于我们终于把文章最前面的那句话解释清楚了,当然中间还有很多的废话,不过我估计能耐着性子看到这里的人会很少,所以如果你看到了这里,可以在底下的评论里面将 index ++ ;这里 index = 0 ;就是看看几个人是真正认真看了这篇文章的。
接着 performTraversals 我们继续说,话说在第一篇文章 我们有讲到,Activity 在启动时,会在第二次执行 performTraversals 才会去真正的绘制,原因在于第一次执行 performTraversals 的时候,会走到 Egl 初始化的逻辑,然后会重新执行一次 performTraversals 。
所以前一篇文章的评论区有人问为何在 run 方法里面还要 post 一次,如果在 run 方法里面直接执行 updateText 方法 ,那么 updateText 就会在第一个 performTraversals 之后就执行,而不是在第一帧绘制完成后才去执行,所以我们又 Post 了一次 。所以大概的处理步骤如下:
第一步:Activity.onCreate –> Activity.onStart –> Activity.onResume
第二步:ViewRootImpl.performTraversals –>Runnable
第三步:Runnable –> ViewRootImpl.performTraversals
第四步:ViewRootImpl.performTraversals –> UpdateText
第五步:UpdateText
其实一路跟下来发现其实原理很简单,其实 DelayLoad 其实只是一个很小的点,关键是教大家如何去跟踪一个自己不认识的知识点或者优化,这里面主要用到了两个工具:Systrace 和 Method Trace, 以及源码编译和调试。
关于 Systrace 和 Method Trace 的使用,之后会有详细的文章去介绍,这两个工具非常有助于理解源码和一些技术的实现。
本文章所所涉及到的代码我放到了Github上:
https://github.com/Gracker/DelayLoadSample
]]>一个人可以走的更快 , 一群人可以走的更远
不用一一去解释,做过启动优化的估计都使用过,本篇文章将详细讲解一下一种延时加载的实现以及其原理.
其实这种加载的实现是非常简单的,但是其中的原理可能比较复杂,还涉及到Looper/Handler/MessageQueue/VSYNC等.以及其中碰到的一些问题,还会有一些我自己额外的思考.
一提到DelayLoad,大家可能第一时间想到的就是在 onCreate 里面调用 Handler.postDelayed方法, 将需要 Delay 加载的东西放到这里面去初始化, 这个也是一个比较方便的方法. Delay一段时间再去执行,这时候应用已经加载完成,界面已经显示出来了, 不过这个方法有一个致命的问题: 延迟多久?
大家都知道,在 Android 的高端机型上,应用的启动是非常快的 , 这时候只需要 Delay 很短的时间就可以了, 但是在低端机型上,应用的启动就没有那么快了,而且现在应用为了兼容旧的机型,往往需要 Delay 较长的时间,这样带来体验上的差异是很明显的.
这里先说优化方案:
首先 , 创建 Handler 和 Runnable 对象, 其中 Runnable 对象的 run方法里面去更新 UI 线程.
1 | private Handler myHandler = new Handler(); |
在主 Activity 的 onCreate 中加入下面的代码
1 | getWindow().getDecorView().post(new Runnable() { |
其实实现的话非常简单,我们来对比一下三种方案的效果.
为了验证我们优化的 DelayLoad的效果,我们写了一个简单的app , 这个 App 中包含三张不同大小的图片,每张图片下面都会有一个 TextView , 来标记图片的显示高度和宽度. MainActivity的代码如下:
1 | public class MainActivity extends AppCompatActivity { |
我们需要关注两个点:
updateText执行的时机?
下面是第一种写法的Trace图:
可以看到 updateText 是在 Activity 的 onCreate/onStart/onResume三个回调执行完成后才去执行的.
图片的宽高是否正确显示?
从图片看一看到,宽高并没有显示. 这是为什么呢? 这个问题就要从Activity 的 onCreate/onStart/onResume三个回调说起了. 其实Activity 的 onCreate/onStart/onResume三个回调中,并没有执行Measure和Layout操作, 这个是在后面的performTraversals中才执行的. 所以在这之前宽高都是0.
第二种写法我们Delay了300ms .我们来看一下表现.
可以看到,这种写法的话,updateText是在两个performTraversals 执行完成之后(这时候 APP 的第一帧才显示出来)才去执行的, 执行完成之后又调用了一次 performTraversals 将 TextView 的内容进行更新.
从上图可以看到,图片的宽高是正确显示了出来. 原因上面已经说了,measure/layout执行完成后,宽高的数据就可以获取了.
有人会说:可以把Delay的时间减小一点嘛,这样就不会闪了. 话是这么说,但是由于 Android 机器的多元性(其实就是有很多高端机器,也有很多低端机器) , 在这个机子上300ms的延迟算是快,在另外一个机子上300ms算是很慢.
我们将Delay时间调整为50ms, 其Trace图如下:
可以看到,updateText 方法在第一个 performTraversals 之后就执行了,所以也没有 Delay Load 的效果(虽然宽高是正确显示了,因为在第一个 performTraversals 方法中就执行了layout和measure).
经过前两个方法 , 我们就会想, 如果能不使用Delay方法, updateText 方法能在 第二个performTraversals 方法执行完成后(即APP第一帧在屏幕上显示),马上就去执行,那么即起到了 Delay Load的作用,又可以正确显示图片的宽高.
第三种写法就是这个效果:
可以看到这种写法. updateText 在第二个 performTraversals 方法执行完成后马上就执行了, 然后下一个 VSYNC 信号来了之后, TextView就更新了.
关于优化的 Delay Load 的实现,从代码层面来看其实是非常简单的.其带来的效果也是很赞的.
但是实现之后我们还需要思考一下,为何这么做就可以实现这种功能呢?很显然要回答这个问题,我们需要知道更底层的一些东西.这个还涉及到 Handler/Message/MessageQueue/Looper/VSYNC/ViewRootImpl等知识. 往大里说应该还涉及到AMS/WMS等.由于涉及到的东西比较多,我就不在这一篇里面阐述了, 下一篇文章将会从从原理上讲解一下为何优化的 Delay Load 会起作用.
本文章所所涉及到的代码我放到了Github上:
https://github.com/Gracker/DelayLoadSample
]]>一个人可以走的更快 , 一群人可以走的更远
本篇文章是自己的一个学习笔记,记录了 Android 5.0 中 hwui 中的 RenderThread 的简单工作流程。由于是学习笔记,所以其中一些细节不会太详细,我只是将大概的流程走一遍,将其工作流标注出来,下次遇到问题的时候就可以知道去哪里查。
下图是我用 Systrace 抓取的一个应用启动的时候 RenderThread 的第一次 Draw 的 Trace 图,从这里面的顺序来看 RenderThread 的流程。熟悉应用启动流程的话应该知道,只有当第一次 DrawFrame 完成之后,整个应用的界面才会显示在手机上,在这之前,用户看到的是应用的 StartingWindow 的界面。
应用程序的每一帧是从接收到 VSYNC 信号开始进行计算和绘制的,这要从 Choreographer 这个类说起了,不过由于篇幅原因,我们直接看一帧的绘制调用关系链即可:
Choreographer 的 drawFrame 会调用到 ViewRootImpl 的 performTraversals 方法,而 performTraversals 方法最终会调用到performDraw() 方法, performDraw 又会调用到 draw(boolean fullRedrawNeeded) 方法,这个 draw 方法是 ViewRootImpl 的私有方法,和我们熟知的那个draw并不是同一个方法
1 | if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { |
如果是走硬件绘制路线的话,则会走这一条先,之后就会调用 mHardwareRenderer 的 draw 方法,这里的 mHardwareRenderer 指的是 ThreadedRenderer ,其 Draw 函数如下:
1 |
|
这个函数里面的 updateRootDisplayList(view, callbacks) ;即 getDisplayList 操作。接下来就是比较重要的一个操作:
1 | int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos, |
可以看出这是一个阻塞操作,等Native层完成后,拿到返回值后才会进行下一步的操作。
其Native代码在android_view_ThreadedRenderer.cpp中,对应的实现代码如下:
1 | static int android_view_ThreadedRenderer_syncAndDrawFrame(JNIEnv* env, jobject clazz, |
RenderProxy的路径位于frameworks/base/libs/hwui/renderthread/RenderProxy.cpp
1 | int RenderProxy::syncAndDrawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos, |
其中 mDrawFrameTask 是一个 DrawFrameTask 对象,其路径位于frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp,其中drawFrame代码:
1 | int DrawFrameTask::drawFrame(nsecs_t frameTimeNanos, nsecs_t recordDurationNanos) { |
其中 postAndWait() 的实现如下:
1 | void DrawFrameTask::postAndWait() { |
就是将一个 DrawFrameTask 放入到了 mRenderThread 中,其中 queue 方法实现如下:
1 | void RenderThread::queue(RenderTask* task) { |
其中 mQueue 是一个 TaskQueue 对象,其
1 | void TaskQueue::queue(RenderTask* task) { |
接着看 RenderThread 之前的 queue 方法,
1 | void Looper::wake() { |
wake 函数则更为简单,仅仅向管道的写端写入一个字符“W”,这样管道的读端就会因为有数据可读而从等待状态中醒来。
接下来会到哪里去,我们首先要熟悉一下RenderThread,RenderThread是继承自Thread的,这个Thread是utils/Thread.h,RenderThread的初始化函数
1 | RenderThread::RenderThread() : Thread(true), Singleton<RenderThread>() |
其 run 方法在 Thread 中有说明:
1 | // Start the thread in threadLoop() which needs to be implemented. |
即启动 threadLoop 函数,我们来看 RenderThread 的 threadLoop 函数,这个函数比较重要:
1 | bool RenderThread::threadLoop() { |
可以看到,一个 for 循环是一个无限循环,而其中 pollOnce 是一个阻塞函数,直到我们上面调用了 mLooper->wake() 之后,会继续往下走,走到 while 循环中:
1 | while (RenderTask* task = nextTask(&nextWakeup)) { |
会将 RenderTask 取出来执行其 run 方法,经过前面的流程我们知道这个 RenderTask 是一个 DrawFrameTask ,其run方法如下:
1 | void DrawFrameTask::run() { |
上面说到了 DrawFrameTask 的 run 方法,这里 run 方法中的执行的方法即我们在最前面那张图中所示的部分(即文章最前面那张图),下面的流程就是那张图中的函数调用,我们结合代码和图,一部分一部分来走整个 DrawFrame 的流程:
第一个比较重要的函数是 syncFrameState ,从函数名就可以知道, syncFrameState 的作用就是同步 frame 信息,将 Java 层维护的 frame 信息同步到 RenderThread中。
Main Thread 和Render Thread 都各自维护了一份应用程序窗口视图信息。各自维护了一份应用程序窗口视图信息的目的,就是为了可以互不干扰,进而实现最大程度的并行。其中,Render Thread维护的应用程序窗口视图信息是来自于 Main Thread 的。因此,当Main Thread 维护的应用程序窗口信息发生了变化时,就需要同步到 Render Thread 去。
所以查看代码就可以知道有两个 RenderNode,一个在 hwui 中,一个在 View 中。简单来说,同步信息就是将 Java 层的 RenderNode 中的信息同步到 hwui 中的 RenderNode 中。 注意syncFrameState的返回值赋给了 canUnblockUiThread ,从名字可以看出这个 canUnblockUiThread 的作用是判断是否唤醒 Main Thread ,也就是说如果返回为 true 的话,会提前唤醒主线程来执行其他的事情,而不用等到 draw 完成后再去唤醒 Main Thread。 这也是 Android 5.0 和 Android 4.x 最大的区别了。
1 | bool DrawFrameTask::syncFrameState(TreeInfo& info) { |
首先是makeCurrent,这里的mContext是一个CanvasContext对象,其makeCurrent实现如下:
1 | void CanvasContext::makeCurrent() { |
mEglManager是一个EglManager对象,其实现为:
1 | bool EglManager::makeCurrent(EGLSurface surface) { |
这里会判断mCurrentSurface == surface,如果成立,则不用再初始化操作,如果是另外一个surface。,则会执行eglMakeCurrent,来重新创建上下文。
makeCurrent之后,会调用mContext->prepareTree(info),其实现如下:
1 | void CanvasContext::prepareTree(TreeInfo& info) { |
其中 mRootRenderNode->prepareTree(info) 又是最重要的。回到Java层,我们知道 ThreadedRenderer 在初始化时,初始化了一个指针
1 | long rootNodePtr = nCreateRootRenderNode(); |
这个RootRenderNode也就是一个根Node,
1 | mRootNode = RenderNode.adopt(rootNodePtr); |
然后会创建一个 mNativeProxy 指针,在 Native 层初始化一个 RenderProxy 对象,将 rootNodePtr 传给 RenderProxy 对象,这样在 RenderProxy 我们就可以得到这个对象的指针了。其中 CanvasContext 也是在 RenderProxy 对象初始化的时候被初始化的,初始化的时候将 rootNodePtr 传给了 CanvasContext 对象。
我们之前提到 ThreadedRenderer 的 draw 方法中首先会调用updateRootDisplayList,即我们熟悉的 getDisplayList 。这个方法中,其实也分为两个步骤,第一个步骤是 updateViewTreeDisplayList,第二个步骤是将根 Node 加入到 DrawOp 中:
1 | canvas.insertReorderBarrier(); |
其最终实现在
1 | status_t DisplayListRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t flags) { |
再回到我们之前的 CanvasContext.prepareTree 中提到的 mRootRenderNode->prepareTree(info),这时候这里的 mRootRenderNode 就是 CanvasContext 初始化是传进来的。
其实现在 RenderNode.cpp 中:
1 | void RenderNode::prepareTree(TreeInfo& info) { |
这里所涉及到的进一步的具体操作大家可以自行去看代码。
执行完syncFrameState之后,接下来就是执行draw
1 | if (CC_LIKELY(canDrawThisFrame)) { |
CanvasContext的draw函数是一个核心函数,其位置在 frameworks/base/libs/hwui/OpenGLRenderer.cpp ,其实现如下:
1 | void CanvasContext::draw() { |
首先来看eglBeginFrame的实现
1 | void EglManager::beginFrame(EGLSurface surface, EGLint* width, EGLint* height) { |
makeCurrent是用来管理上下文,eglBeginFrame主要是校验参数的合法性。
1 | status_t status; |
这里的mCanvas是一个OpenGLRenderer对象,其prepareDirty实现
1 | //TODO:增加函数功能描述 |
1 | Rect outBounds; |
接下来就是调用OpenGLRenderer的drawRenderNode方法进行绘制
1 | status_t OpenGLRenderer::drawRenderNode(RenderNode* renderNode, Rect& dirty, int32_t replayFlags) { |
这里的 renderNode 是一个 Root Render Node,
可以看到,到了这里虽然只是开始,但是其实已经结束了,这个函数里面最重要的几步:
1 | renderNode->defer(deferStruct, 0); //进行重排序 |
这几个是渲染部分真正的核心部分,其中的代码细节需要自己去研究。老罗在这部分讲的很细,有空可以去看看他的文章Android应用程序UI硬件加速渲染的Display List渲染过程分析.
1 | if (status & DrawGlInfo::kStatusDrew) { |
其核心就是调用EGL的 eglSwapBuffers(mEglDisplay, surface), duration)函数。
1 | profiler().finishFrame(); |
主要是记录时间信息。
鉴于我比较懒,而且总结能力不如老罗,就直接把他的总结贴过来了。
RenderThread的总的流程如下:
- 将Main Thread维护的Display List同步到Render Thread维护的Display List去。这个同步过程由Render Thread执行,但是Main Thread会被阻塞住。
- 如果能够完全地将Main Thread维护的Display List同步到Render Thread维护的Display List去,那么Main Thread就会被唤醒,此后Main Thread和Render Thread就互不干扰,各自操作各自内部维护的Display List;否则的话,Main Thread就会继续阻塞,直到Render Thread完成应用程序窗口当前帧的渲染为止。
- Render Thread在渲染应用程序窗口的Root Render Node的Display List之前,首先将那些设置了Layer的子Render Node的Display List渲染在各自的一个FBO上,接下来再一起将这些FBO以及那些没有设置Layer的子Render Node的Display List一起渲染在Frame Buffer之上,也就是渲染在从Surface Flinger请求回来的一个图形缓冲区上。这个图形缓冲区最终会被提交给Surface Flinger合并以及显示在屏幕上。
第2步能够完全将Main Thread维护的Display List同步到Render Thread维护的Display List去很关键,它使得Main Thread和Render Thread可以并行执行,这意味着Render Thread在渲染应用程序窗口当前帧的Display List的同时,Main Thread可以去准备应用程序窗口下一帧的Display List,这样就使得应用程序窗口的UI更流畅。
注意最后一段,在 Android 4.x 时代,没有RenderThread的时代,只有 Main Thread ,也就是说 必须要等到 Draw 完成后,才会去准备下一帧的数据,如下图:
Android5.0 之后,如老罗所说,有两种情况,
可以看到第二张图中,Render Thread 并没有绘制完成,但是由于其提前唤醒了 Main Thread ,所以 Main Thread 在下一个Vsync信号到来的时候,响应了Vsync事件,开始准备下一帧。
此时虽然由于第一帧绘制时间过长,导致掉了一帧,但是第二帧没有收到任何影响。
]]>一个人可以走的更快 , 一群人可以走的更远
HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
HashMap使用数组和链表来共同组成的。可以看出底层是一个数组,而数组的每个元素都是一个链表头。
1 | static class Entry<K,V> implements Map.Entry<K,V> { |
Entry是HashMap中的一个内部静态类,包级私有,实现了Map中的接口Entey<K,V>。可以看出来它内部含有一个指向下一个元素的指针。
HashMap的构造函数有四个:
实际上就两种,一个是指定初始容量和加载因子,一个是用一个给定的映射关系生成一个新的HashMap。说一下第一种。
1 | /** |
参数很简单,初始容量,和加载因子。初始容量定义了初识数组的大小,加载因子和初始容量的乘积确定了一个阈值。阈值最大是(1<<30) + 1。初始容量一定是2的N次方,而且刚刚比要设置的值大。默认初始容量是16,默认加载因子是0.75。当表中的元素数量大于等于阈值时,数组的容量会翻倍,并重新插入元素到新的数组中,所以HashMap不保证顺序恒久不变。
当输入的加载因子小于零或者不是浮点数时会抛出异常(IllegalArgumentException)。
1 | /** |
由于HashMap只是key值为null,所以首先要判断key值是不是为null,是则进行特殊处理。
1 | /** |
可以看出key值为null则会插入到数组的第一个位置。如果第一个位置存在,则替代,不存在则添加一个新的。稍后会看到addEntry函数。
** PS:考虑一个问题,key值为null会插入到table[0],那为什么还要遍历整个链表呢?**
回到put函数中。在判断key不为null后,会求key的hash值,并通过indexFor函数找出这个key应该存在table中的位置。
1 | /** |
indexFor函数很简短,但是却实现的很巧妙。一般来说我们把一个数映射到一个固定的长度会用取余(%)运算,也就是h % length,但里巧妙地运用了table.length的特性。还记得前面说了数组的容量都是很特殊的数,是2的N次方。用二进制表示也就是一个1后面N个0,(length-1)就是N个1了。这里直接用与运算,运算速度快,效率高。但是这是是利用了length的特殊性,如果length不是2的N次方的话可能会增加冲突。
前面的问题在这里就有答案了。因为indexFor函数返回值的范围是0到(length-1),所以可能会有key值不是null的Entry存到table[0]中,所以前面还是需要遍历链表的。
得到key值对应在table中的位置,就可以对链表进行遍历,如果存在该key则,替换value,并把旧的value返回,modCount++代表操作数加1。这个属性用于Fail-Fast机制,后面讲到。如果遍历链表后发现key不存在,则要插入一个新的Entry到链表中。这时就会调用addEntry函数
1 | /** |
这个函数有四个参数,第一个是key的hash值,第二个第三个分别是key和value,最后一个是这个key在table中的位置,也就是indexFor(hash(key), table.length-1)。首先会判断size(当前表中的元素个数)是不是大于或等于阈值。并且判断数组这个位置是不是空。如果条件满足则要resize(2 * table. length),等下我们来看这个操作。超过阈值要resize是为了减少冲突,提高访问效率。判断当前位置不是空时才resize是为了尽可能减少resize次数,因为这个位置是空,放一个元素在这也没有冲突,所以不影响效率,就先不进行resize了。
1 | /** |
resize操作先要判断当前table的长度是不是已经等于最大容量(1<<30)了,如果是则把阈值调到整数的最大值((1<<31) - 1),就没有再拓展table的必要了。如果没有到达最大容量,就要生成一个新的空数组,长度是原来的两倍。这时候可能要问了,如果oldTable. length不等于MAXIMUM_CAPACITY,但是(2 * oldTable. length)也就是newCapacity大于MAXIMUM_CAPACITY怎么办?这个是不可能的,因为数组长度是2的N次方,而MAXIMUM_CAPACITY = 1<<30。
生成新的数组后要执行transfer函数。
1 | /** |
这个函数要做的就是把原来table中的值挨个拿出来插到新数组中,由于数组长度发生了改变,所以元素的位置肯定发生变化,所以HashMap不能保证该顺序恒久不变。回到resize函数,这时新的数组已经生成了,只需要替换原来数组就好了。并且要更新一下阈值。可以看出来resize是个比较消耗资源的函数,所以能减少resize的次数就尽量减少。
回到函数addEntry 中,判断完是不是需要resize后就需要创建一个新的Entry了。
1 | /** |
调用createEntry函数,参数跟addEntry一样,第一个是key的hash值,第二个第三个分别是key和value,最后一个是这个key在table中的位置。这里的操作与Entry的构造函数有关系。
1 | /** |
构造函数中传入一个Entry对象,并把它当做这个新生成的Entry的next。所以createEntry函数中的操作相当于把table[bucketIndex]上的链表拿下来,放在新的Entry后面,然后再把新的Entry放到table[bucketIndex]上。
到这里整个put函数算是结束了。如果新插入的K,V则会返回null。
1 | /** |
也是先判断key是不是null,做特殊处理。直接上代码,不赘述。
1 | /** |
key不是null则会调用getEntry函数,并返回一个Entry对象,如果不是null,就返回entry的value。
1 | /** |
直接求key值hash值,然后求table中的位置,遍历链表。有返回entry对象,没有返回null。
1 | /** |
我们知道java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这一策略在源码中的实现是通过modCount域,保证线程之间修改的可见性。,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
]]>一个人可以走的更快 , 一群人可以走的更远
OnTrimMemory 回调是 Android 4.0 之后提供的一个API,这个 API 是提供给开发者的,它的主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死。这样应用在下一次启动的时候,速度就会比较快。
本文通过问答的方式,从各个方面来讲解 OnTrimMemory 回调的使用过程和效果。想要开发高性能且用户体验良好的 Android 应用,那么这篇文章你不应该错过。
OnTrimMemory是Android在4.0之后加入的一个回调,任何实现了ComponentCallbacks2接口的类都可以重写实现这个回调方法.OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.
Android系统会根据不同等级的内存使用情况,调用这个函数,并传入对应的等级:
下面三个等级是当我们的应用程序真正运行时的回调:
当应用程序是缓存的,则会收到以下几种类型的回调:
通常在架构阶段就要考虑清楚,我们有哪些东西是要常驻内存的,有哪些是伴随界面存在的.一般情况下,有下面几种资源需要进行释放:
代码出处:Launcher
Launcher.java:
1 |
|
AppsCustomizeTabHost.java:
1 | public void onTrimMemory() { |
AppsCustomizePagedView.java:
1 | public void clearAllWidgetPages() { |
PagedViewGridLayout.java
1 |
|
代码出处:Contact
1 | @Override |
onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发,这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。
因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。
需要注意的是,onTrimMemory的TRIM_MEMORY_UI_HIDDEN 等级是在onStop方法之前调用的.
在引入OnTrimMemory之前都是使用OnLowMemory回调,需要知道的是,OnLowMemory大概和OnTrimMemory中的TRIM_MEMORY_COMPLETE级别相同,如果你想兼容api<14的机器,那么可以用OnLowMemory来实现,否则你可以忽略OnLowMemory,直接使用OnTrimMemory即可.
尽管系统在内存不足的时候杀进程的顺序是按照LRU Cache中从低到高来的,但是它同时也会考虑杀掉那些占用内存较高的应用来让系统更快地获得更多的内存。
所以如果你的应用占用内存较小,就可以增加不被杀掉的几率,从而快速地恢复(如果不被杀掉,启动的时候就是热启动,否则就是冷启动,其速度差在2~3倍)。
所以说在几个不同的OnTrimMemory回调中释放自己的UI资源,可以有效地提高用户体验。
一些常驻内存的应用,比如Launcher、安全中心、电话等,在用户使用过要退出的时候,需要调用OnTrimMemory来及时释放用户使用的时候所产生的多余的内存资源:比如动态生成的View、图片缓存、Fragment等。
这些应用不是常驻内存的,意味着可以被任务管理器杀掉,但是在某些场景下用户不会去杀。
这类应用包括:音乐、下载等。用户退出UI界面后,音乐还在继续播放,下载程序还在运行。这时候音乐应该释放部分UI资源和Cache。
]]>一个人可以走的更快 , 一群人可以走的更远
这篇文章主要介绍在实际Android应用程序的开发中,容易导致内存泄露的一些情况。开发人员如果在进行代码编写之前就有内存泄露方面的基础知识,那么写出来的代码会强壮许多,写这篇文章也是这个初衷。本文从Android开发中的资源使用情况入手,介绍了如何在Bitmap、数据库查询、9-patch、过渡绘制等方面优化内存的使用。
Android中的大部分内存问题归根结底都是Bitmap的问题,如果打开MAT(Memory analyzer tool)来看,实际占用内存大的都是一些Bitmap(以byte数组的形式存储)。所以Bitmap的优化应该是我们着重去解决的。Google在其官方有针对Bitmap的使用专门写了一个专题 : Displaying Bitmaps Efficiently, 对应的中文翻译在 :displaying-bitmaps , 在优化Bitmap资源之前,请先看看这个系列的文档,以确保自己正确地使用了Bitmap。
Bitmap如果没有被释放,那么一般只有两个问题:
当你确定这个Bitmap资源不会再被使用的时候(当然这个Bitmap不释放可能会让程序下一次启动或者resume快一些,但是其占用的内存资源太大,可能导致程序在后台的时候被杀掉,反而得不偿失),我们建议手动调用recycle()方法,释放其Native内存:
1 | if(bitmap != null && !bitmap.isRecycled()){ |
我们也可以看一下Bitmap.java中recycle()方法的说明:
1 | /** |
调用bitmap.recycle之后,这个Bitmap如果没有被引用到,那么就会被垃圾回收器回收。如果不主动调用这个方法,垃圾回收器也会进行回收工作,只不过垃圾回收器的不确定性太大,依赖其自动回收不靠谱(比如垃圾回收器一次性要回收好多Bitmap,那么需要的时间就会很多,导致回收的时候会卡顿)。所以我们需要主动调用recycle。
由于我们在实际开发中,很多情况是在xml布局文件中设置ImageView的src或者在代码中调用ImageView.setImageResource/setImageURI/setImageDrawable等方法设置图像,下面代码可以回收这个ImageView所对应的资源:
1 | private static void recycleImageViewBitMap(ImageView imageView) { |
如果你的ImageView是有Background,那么下面的代码可以释放他:
1 | public static void recycleBackgroundBitMap(ImageView view) { |
现在手机的分辨率越来越高,图片资源在被加载后所占用的内存也越来越大,所以要尽量避免使用大的PNG图,在产品设计的时候就要尽量避免用一张大图来进行展示,尽量多用NinePatch资源。
Android中的NinePatch指的是一种拉伸后不会变形的特殊png图,NinePatch的拉伸区域可以自己定义。这种图的优点是体积小,拉伸不变形,可以适配多机型。Android SDK中有自带NinePatch资源制作工具,Android-Studio中在普通png图片点击右键可以将其转换为NinePatch资源,使用起来非常方便。
图片有不同的形状与大小。在大多数情况下它们的实际大小都比需要呈现出来的要大很多。例如,系统的Gallery程序会显示那些你使用设备camera拍摄的图片,但是那些图片的分辨率通常都比你的设备屏幕分辨率要高很多。
考虑到程序是在有限的内存下工作,理想情况是你只需要在内存中加载一个低分辨率的版本即可。这个低分辨率的版本应该是与你的UI大小所匹配的,这样才便于显示。一个高分辨率的图片不会提供任何可见的好处,却会占用宝贵的(precious)的内存资源,并且会在快速滑动图片时导致(incurs)附加的效率问题。
Google官网的Training中,有一篇文章专门介绍如何有效地加载大图,里面提到了两个比较重要的技术:
原文地址:Loading Large Bitmaps Efficiently,中文翻译地址:有效地加载大尺寸位图,强烈建议每一位Android开发者都去看一下,并在自己的实际项目中使用到。
更多关于Bitmap的使用和优化,可以参考Android官方Training专题的displaying-bitmaps
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:
1 | Cursor cursor = getContentResolver().query(uri ...); |
修正示例代码:
1 | Cursor cursor = null; |
`
以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
1 | public View getView(int position, View convertView, ViewGroup parent) |
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。
由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list item的view对象的过程可以查看:android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。
示例代码:
1 | public View getView(int position, View convertView, ViewGroup parent) { |
`
示例修正代码:
1 | public View getView(int position, View convertView, ViewGroup parent) { |
关于ListView的使用和优化,可以参考这两篇文章:
前面有说过,一个对象的内存没有被释放是因为他被其他的对象所引用,系统不回去释放这些有GC Root的对象。
示例A:
假设有如下操作
1 | public class DemoActivity extends Activity { |
我们有一个成员变量 obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:
1 | public void operation() { |
示例B:
假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。
但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_ui进程挂掉。
总之当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。
使用MAT可以很方便地查看对象之间的引用,
Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、onDestroy()方法中需要适当的释放资源的情况。由于此情况很基础,在此不详细说明,具体可以查看官方文档对Activity生命周期的介绍,以明确何时应该释放哪些资源。
过渡绘制指的是在屏幕一个像素上绘制多次(超过一次),比如一个TextView后有背景,那么显示文本的像素至少绘了两次,一次是背景,一次是文本。GPU过度绘制或多或少对性能有些影响,设备的内存带宽是有限的,当过度绘制导致应用需要更多的带宽(超过了可用带宽)的时候性能就会降低。带宽的限制每个设备都可能是不一样的。
过渡绘制的原因:
减少过渡绘制能去掉一些无用的View,能有效减少GPU的负载,也可以减轻一部分内存压力。关于过渡绘制我专门写了一篇文章来介绍:过渡绘制及其优化
在Android应用开发过程中,屏幕上控件的布局代码和程序的逻辑代码通常是分开的。界面的布局代码是放在一个独立的xml文件中的,这个文件里面是树型组织的,控制着页面的布局。通常,在这个页面中会用到很多控件,控件会用到很多的资源。Android系统本身有很多的资源,包括各种各样的字符串、图片、动画、样式和布局等等,这些都可以在应用程序中直接使用。这样做的好处很多,既可以减少内存的使用,又可以减少部分工作量,也可以缩减程序安装包的大小。
比如下面的代码就是使用系统的ListView:
1 | <ListView |
在开发中,不可能保证一次就开发出一个内存管理非常棒的应用,所以在开发的每一个阶段,都要有意识地去针对内存进行专门的检查。目前Android提供了许多布局、内存相关的工具,比如Lint、MAT等。学会这些工具的使用是一个Android开发者必不可少的技能。
]]>一个人可以走的更快 , 一群人可以走的更远
为了使垃圾回收器可以正常释放程序所占用的内存,在编写代码的时候就一定要注意尽量避免出现内存泄漏的情况(通常都是由于全局成员变量持有对象引用所导致的),并且在适当的时候去释放对象引用。对于大多数的应用程序而言,后面其它的事情就可以都交给垃圾回收器去完成了,如果一个对象的引用不再被其它对象所持有,那么系统就会将这个对象所分配的内存进行回收。
我们在开发软件的时候应当自始至终都把内存的问题充分考虑进去,这样的话才能开发出更加高性能的软件。而内存问题也并不是无规律可行的,Android系统给我们提出了很多内存优化的建议技巧,只要按照这些技巧来编写程序,就可以让我们的程序在内存性能发面表现得相当不错。
本文原文来自Android开发者官网Managing Your App’s Memory章节中的
How Your App Should Manage Memory部分。是Android官方帮助应用开发者更好地管理应用的内存而写的。作为一个应用程序开发者,你需要在你开发应用程序的时时刻刻都考虑内存问题。
如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况。
当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存。并且,系统可以在LRU cache当中缓存的进程数量也会减少,导致切换应用程序的时候耗费更多性能。严重的话,甚至有可能会导致崩溃,因为系统在内存非常吃紧的时候可能已无法维护所有正在运行的Service所依赖的进程了。
为了能够控制Service的生命周期,Android官方推荐的最佳解决方案就是使用IntentService,这种Service的最大特点就是当后台任务执行结束后会自动停止,从而极大程度上避免了Service内存泄漏的可能性。
让一个Service在后台一直保持运行,即使它并不执行任何工作,这是编写Android程序时最糟糕的做法之一。所以Android官方极度建议开发人员们不要过于贪婪,让Service在后台一直运行,这不仅可能会导致手机和程序的性能非常低下,而且被用户发现了之后也有可能直接导致我们的软件被卸载
当用户打开了另外一个程序,我们的程序界面已经不再可见的时候,我们应当将所有和界面相关的资源进行释放。在这种场景下释放资源可以让系统缓存后台进程的能力显著增加,因此也会让用户体验变得更好。
那么我们如何才能知道程序界面是不是已经不可见了呢?其实很简单,只需要在Activity中重写onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别,一旦触发了之后就说明用户已经离开了我们的程序,那么此时就可以进行资源释放操作了,如下所示:
1 |
|
注意onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发,这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。
除了刚才讲的TRIM_MEMORY_UI_HIDDEN这个回调,onTrimMemory()方法还有很多种其它类型的回调,可以在手机内存降低的时候及时通知我们。我们应该根据回调中传入的级别来去决定如何释放应用程序的资源:
TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。
TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。
因为onTrimMemory()是在API14才加进来的,所以如果要支持API14之前的话,则可以考虑 onLowMemory()这个方法,它粗略的相等于onTrimMemory()回调的TRIM_MEMORY_COMPLETE事件。
注意:当系统安装LRU cache杀进程的时候,尽管大部分时间是从下往上按顺序杀,有时候系统也会将占用内存比较大的进程纳入被杀范围,以尽快得到足够的内存。所以你的应用在LRU list中占用的内存越少,你就越能避免被杀掉,当你恢复的时候也会更快。
正如前面提到的,每一个Android设备都会有不同的RAM总大小与可用空间,因此不同设备为app提供了不同大小的heap限制。你可以通过调用getMemoryClass()来获取你的app的可用heap大小。如果你的app尝试申请更多的内存,会出现OutOfMemory的错误。
在一些特殊的情景下,你可以通过在manifest的application标签下添加largeHeap=true的属性来声明一个更大的heap空间。如果你这样做,你可以通过getLargeMemoryClass()来获取到一个更大的heap size。
然而,能够获取更大heap的设计本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用大量的内存而去请求一个大的heap size。只有当你清楚的知道哪里会使用大量的内存并且为什么这些内存必须被保留时才去使用large heap. 因此请尽量少使用large heap。使用额外的内存会影响系统整体的用户体验,并且会使得GC的每次运行时间更长。在任务切换时,系统的性能会变得大打折扣。
另外, large heap并不一定能够获取到更大的heap。在某些有严格限制的机器上,large heap的大小和通常的heap size是一样的。因此即使你申请了large heap,你还是应该通过执行getMemoryClass()来检查实际获取到的heap大小。
当我们读取一个Bitmap图片的时候,有一点一定要注意,就是千万不要去加载不需要的分辨率。在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存。需要仅记的一点是,将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的,比如这张图片是15001000像素,使用的ARGB_8888颜色类型,那么每个像素点就会占用4个字节,总内存就是15001000*4字节,也就是5.7M,这个数据看起来就比较恐怖了。
利用Android Framework里面优化过的容器类,例如SparseArray, SparseBooleanArray, 与 LongSparseArray。 通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。
我们还应当清楚我们所使用语言的内存开支和消耗情况,并且在整个软件的设计和开发当中都应该将这些信息考虑在内。可能有一些看起来无关痛痒的写法,结果却会导致很大一部分的内存开支,例如:
许多程序员都喜欢各种使用抽象来编程,认为这是一种很好的编程习惯。当然,这一点不可否认,因为的抽象的编程方法更加面向对象,而且在代码的维护和可扩展性方面都会有所提高。但是,在Android上使用抽象会带来额外的内存开支,因为抽象的编程方法需要编写额外的代码,虽然这些代码根本执行不到,但是却也要映射到内存当中,不仅占用了更多的内存,在执行效率方面也会有所降低。当然这里我并不是提倡大家完全不使用抽象编程,而是谨慎使用抽象编程,不要认为这是一种很酷的编程方式而去肆意使用它,只在你认为有必要的情况下才去使用。
Protocol buffers是由Google为序列化结构数据而设计的,一种语言无关,平台无关,具有良好扩展性的协议。类似XML,却比XML更加轻量,快速,简单。如果你需要为你的数据实现协议化,你应该在客户端的代码中总是使用nano protobufs。通常的协议化操作会生成大量繁琐的代码,这容易给你的app带来许多问题:增加RAM的使用量,显著增加APK的大小,更慢的执行速度,更容易达到DEX的字符限制。
关于更多细节,请参考protobuf readme的”Nano version”章节。
现在有很多人都喜欢在Android工程当中使用依赖注入框架,比如说像Guice或者RoboGuice等,因为它们可以简化一些复杂的编码操作,比如可以将下面的一段代码:
1 | class AndroidWay extends Activity { |
简化成这样的一种写法:
1 | @ContentView(R.layout.main) |
看上去确实十分诱人,我们甚至可以将findViewById()这一类的繁琐操作全部省去了。但是这些框架为了要搜寻代码中的注解,通常都需要经历较长的初始化过程,并且还可能将一些你用不到的对象也一并加载到内存当中。这些用不到的对象会一直占用着内存空间,可能要过很久之后才会得到释放,相较之下,也许多敲几行看似繁琐的代码才是更好的选择。
很多External library的代码都不是为移动网络环境而编写的,在移动客户端则显示的效率不高。至少,当你决定使用一个external library的时候,你应该针对移动网络做繁琐的porting与maintenance的工作。
即使是针对Android而设计的library,也可能是很危险的,因为每一个library所做的事情都是不一样的。例如,其中一个lib使用的是nano protobufs, 而另外一个使用的是micro protobufs。那么这样,在你的app里面就有2种protobuf的实现方式。这样的冲突同样可能发生在输出日志,加载图片,缓存等等模块里面。
同样不要陷入为了1个或者2个功能而导入整个library的陷阱。如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。
官方有列出许多优化整个app性能的文章:Best Practices for Performance. 这篇文章就是其中之一。有些文章是讲解如何优化app的CPU使用效率,有些是如何优化app的内存使用效率。
你还应该阅读optimizing your UI来为layout进行优化。同样还应该关注lint工具所提出的建议,进行优化。
ProGuard能够通过移除不需要的代码,重命名类,域与方法等方对代码进行压缩,优化与混淆。使用ProGuard可以是的你的代码更加紧凑,这样能够使用更少mapped代码所需要的RAM。
在编写完所有代码,并通过编译系统生成APK之后,你需要使用zipalign对APK进行重新校准。如果你不做这个步骤,会导致你的APK需要更多的RAM,因为一些类似图片资源的东西不能被mapped。
**Notes::**Google Play不接受没有经过zipalign的APK。
一旦你获取到一个相对稳定的版本后,需要分析你的app整个生命周期内使用的内存情况,并进行优化,更多细节请参考Investigating Your RAM Usage.
如果合适的话,有一个更高级的技术可以帮助你的app管理内存使用:通过把你的app组件切分成多个组件,运行在不同的进程中。这个技术必须谨慎使用,大多数app都不应该运行在多个进程中。因为如果使用不当,它会显著增加内存的使用,而不是减少。当你的app需要在后台运行与前台一样的大量的任务的时候,可以考虑使用这个技术。
一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个app运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的app可以切分成2个进程:一个用来操作UI,另外一个用来后台的Service.
你可以通过在manifest文件中声明’android:process’属性来实现某个组件运行在另外一个进程的操作。
1 | <service android:name=".PlaybackService" |
更多关于使用这个技术的细节,请参考原文,链接如下。
http://developer.android.com/training/articles/memory.html
]]>一个人可以走的更快 , 一群人可以走的更远
这篇文章主要是介绍了一些小细节的优化技巧,当这些小技巧综合使用起来的时候,对于整个App的性能提升还是有作用的,只是不能较大幅度的提升性能而已。选择合适的算法与数据结构才应该是你首要考虑的因素,在这篇文章中不会涉及这方面。你应该使用这篇文章中的小技巧作为平时写代码的习惯,这样能够提升代码的效率。
本文的原文为Google官方Training的性能优化部分,这一章节主要讲解的是高性能Android代码优化建议,建议所有Android应用开发者都仔细阅读这份文档,并将所提到的编码思想运用到实际的Android开发中。
原文地址:http://developer.android.com/training/articles/perf-tips.html
通常来说,高效的代码需要满足下面两个规则:
你会面临最棘手的一个问题是当你优化一个肯定会在多种类型的硬件上运行的应用程序。不同版本的VM在不同的处理器上运行速度不同。它甚至不是你可以简单地说“设备X因为F原因比设备Y快/慢”那么简单,而且也不能简单地从一个设备拓展到另一个设备。特别提醒的是模拟器在性能方面和其他的设备没有可比性。通常有JIT优化和没有JIT优化的设备之间存在巨大差异:经过JIT代码优化的设备并不一定比没有经过JIT代码优化的设备好。
代码的执行效果会受到设备CPU,设备内存,系统版本等诸多因素的影响。为了确保代码能够在不同设备上都运行良好,需要最大化代码的效率。
虽然GC可以回收不用的对象,可是为这些对象分配内存,并回收它们同样是需要耗费资源的。
因此请尽量避免创建不必要的对象,有下面一些例子来说明这个问题:
一个稍微激进点的做法是把所有多维的数据分解成1维的数组:
通常来说,需要避免创建更多的对象。更少的对象意味者更少的GC动作,GC会对用户体验有比较直接的影响。
如果你不需要访问一个对象的值域,请保证这个方法是static类型的,这样方法调用将快15%-20%。这是一个好的习惯,因为你可以从方法声明中得知调用无法改变这个对象的状态。
先看下面这种声明的方式
1 | static int intVal = 42; |
编译器会在类首次被使用到的时候,使用初始化<clinit>
方法来初始化上面的值,之后访问的时候会需要先到它那里查找,然后才返回数据。我们可以使用static final来提升性能:
1 | static final int intVal = 42; |
这时再也不需要上面的那个方法来做多余的查找动作了。
** 所以,请尽可能的为常量声明为static final类型的。**
像C++等native language,通常使用getters(i = getCount())而不是直接访问变量(i = mCount).这是编写C++的一种优秀习惯,而且通常也被其他面向对象的语言所采用,例如C#与Java,因为编译器通常会做inline访问,而且你需要限制或者调试变量,你可以在任何时候在getter/setter里面添加代码。
然而,在Android上,这是一个糟糕的写法。Virtual method的调用比起直接访问变量要耗费更多。那么合理的做法是:在面向对象的设计当中应该使用getter/setter,但是在类的内部你应该直接访问变量。
没有JIT(Just In Time Compiler)
时,直接访问变量的速度是调用getter的3倍。有JIT时,直接访问变量的速度是通过getter访问的7倍。
请注意,如果你使用ProGuard, 你可以获得同样的效果,因为ProGuard可以为你inline accessors.
请比较下面三种循环的方法:
1 | static class Foo { |
所以请尽量使用for-each的方法,但是对于ArrayList,请使用方法one()。
参考下面一段代码
1 | public class Foo { |
这里重要的是,我们定义了一个私有的内部类(Foo$Inner),它直接访问了外部类中的私有方法以及私有成员对象。这是合法的,这段代码也会如同预期一样打印出”Value is 27”。
问题是,VM因为Foo和Foo$Inner是不同的类,会认为在Foo$Inner中直接访问Foo类的私有成员是不合法的。即使Java语言允许内部类访问外部类的私有成员。为了去除这种差异,编译器会产生一些仿造函数:
1 | /*package*/ static int Foo.access$100(Foo foo) { |
每当内部类需要访问外部类中的mValue成员或需要调用doStuff()函数时,它都会调用这些静态方法。这意味着,上面的代码可以归结为,通过accessor函数来访问成员变量。早些时候我们说过,通过accessor会比直接访问域要慢。所以,这是一个特定语言用法造成性能降低的例子。
如果你正在性能热区(hotspot:高频率、重复执行的代码段)使用像这样的代码,你可以把内部类需要访问的域和方法声明为包级访问,而不是私有访问权限。不幸的是,这意味着在相同包中的其他类也可以直接访问这些域,所以在公开的API中你不能这样做。
Android系统中float类型的数据存取速度是int类型的一半,尽量优先采用int类型。
尽量使用System.arraycopy()等一些封装好的库函数,它的效率是手动编写copy实现的9倍多。
** Tip: Also see Josh Bloch’s Effective Java, item 47. **
当你需要把已经存在的native code迁移到Android,请谨慎使用JNI。如果你要使用JNI,请学习JNI Tips
在没有做JIT之前,使用一种确切的数据类型确实要比抽象的数据类型速度要更有效率。(例如,使用HashMap要比Map效率更高。) 有误传效率要高一倍,实际上只是6%左右。而且,在JIT之后,他们直接并没有大多差异。
上面文档中出现的数据是Android的实际运行效果。我们可以用Traceview 来测量,但是测量的数据是没有经过JIT优化的,所以实际的效果应该是要比测量的数据稍微好些。
关于如何测量与调试,还可以参考下面两篇文章:
]]>一个人可以走的更快 , 一群人可以走的更远
从图中可以看出,抽屉中的APP是使用字母进行索引的.最上面一排是最常用的的APP,目前是不可以定制的.
另外一个感觉变化比较大的地方就是动画,文件夹动画和应用启动动画都变了,这个不好描述,大家回头可以自己去感受.
设置里面可以设置主题了,不过目前也就light和dark可以选择.
Android M Preview官方固件里面默认是没有多窗口模式这个选项的.因为官方的固件是user版本的,而这个多窗口的模式是在userdebug中才能打开的. 所以必须要手动进行开启.
1 | adb reboot bootloader |
这个twrp是从官网下载的,下载速度略慢,我把它放到了百度网盘,方便大家去下载:twrp
1 | fastboot flash recovery ~/Downloads/twrp-2.8.6.0-shamu.img |
刷入recovery之后不用重启手机,直接进入recovery模式,进入mount,将system这一项前面的对勾选上。这一步是为了可以改system目录里面的值。
第三步结束后,使用adb devices查看是否可以看到手机。如果能正常看到手机,那么先进入shell:
1 | adb shell |
然后使用vi打开system/build.prop
1 | vi system/build.prop |
找到
1 | ro.build.type=user |
将其修改为:
1 | ro.build.type=userdebug |
保存后退出shell,这时候执行adb reboot重启机器就可以看到在开发者选项中多了一项:
开启多窗口的方法是点击多任务按钮,选择一个任务窗口上面的三个框(选一个即可)
,第一个是在上面窗口,第二个是在下面的窗口,第三个是全屏。
然后就可以看到效果了:
说是多窗口,其实目前只支持双窗口。不过双窗口的窗口利用率其实并不高,多窗口就更加呵呵呵了。这个就看Google之后如何发挥这个了。
其实多窗口三星早就实现了。而且每个窗口还可以动态调整大小,不过这并没有什么卵用。测评中提到的也不多。可见多窗口现在其实还只是一个炫技的功能。至于使用的话,可能还得要多迭代几次才可以。
另外这篇文章的简书地址在此:Nexus6-with-Android-M开启多窗口模式 . 因为我写东西都是在简书上完成,然后才Copy到其他的地方,所以这里也给大家推荐一下简书吧。
]]>一个人可以走的更快 , 一群人可以走的更远
单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
中心原则就是:单例对象的类必须保证只有一个实例存在
在java中主要有两种构建方式
简单的说就是一个需要延迟初始化,一个则不需要。
比较简单的构建方式有:
1 | public class Singleton { |
这种方式实现简单,实例在类装载时构建,如果想要实现一种实例在第一次被使用时构建应该怎么做?
有一种叫做 双重检查锁(double-checked locking)
1 | public class Singleton { |
此种方法只能用在JDK5及以后版本(注意 INSTANCE 被声明为 volatile),之前的版本使用“双重检查锁”会发生非预期行为.
在第一条推荐阅读里提到了另一种实现单例的方式 lazy initialization holder class idiom
1 | public class Singleton { |
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。相比其他实现方案(如double-checked locking等),该技术方案的实现代码较为简洁,并且在所有版本的编译器中都是可行的。
关于 static final Singleton INSTANCE 域的访问权限为什么时包级私有可以阅读: Initialization On Demand Holder idiom的实现探讨
最后推荐实现最为简洁的一种方式: 使用枚举
代码极其简洁, 使用极其简单:
1 | public enum Singleton { |
回顾一下前面集中单例的实现方式, 都只考虑了常规获取类对象的手段, 然而还可以通过序列化和反射机制获取对象.上面两种方式如果实现了序列化接口 Serializable 就必须重写 readResolve() 方法
1 | private Object readResolve(){ |
即使重写了 readResolve() 方法也会涉及某系域需要关键字 transient 的修饰, 具体讨论不再展开, 总之涉及序列化挺蛋疼.
关于防止反射暂时没有深入了解, 据了解: 因为反射的某些地方绕过了java机制的限制,private只在编译时进行权限的限制,但是在运行时是不存在这种权限的限制的, 此处仅供参考.
但是使用enum实现的单例自带防序列化与防反射功能, 详细参照枚举类反编译后代码(供参考)
1 | public abstract class Singleton extends Enum |
我们实现的枚举都是继承了 java.lang.Enum 可以看出来单例的实现也是通过关键字 static 修饰的静态初始化块来实现.
那么为什么enum可以防御反射呢…很简单, 因为它是一个抽象类 public abstract class Singleton extends Enum 即使是反射机制也不能实例化了.
有为什么能防御序列化呢…这个要看java源码中对于对象序列化的处理.
1 | java.io.ObjectOutputStream |
可以看出enum在被序列化时时经过特殊处理的, 被序列化的仅仅是枚举的名字而已.所以可以猜测一下反序列的的代码实现
1 | java.io.ObjectOutputStream |
反序列化也仅仅是通过name调用了方法
1 | Enum.valueOf(Class<T> enumType, String name) |
获取了一个枚举实例, 所以枚举也可以防止通过序列化产生新的单例.
友情建议: 在序列化枚举时要特别注意, 枚举的名称一定不能改变, 否则在反序列化时有可能会抛出异常!!!
1 | public static <T extends Enum<T>> T valueOf(Class<T> enumType, |
]]>一个人可以走的更快 , 一群人可以走的更远
“If you can measure it, you can optimize it” is a common term in the computing world, and for Android’s rendering system, the same thing holds true. In order to optimize your pipeline to be more efficient for rendering, you need a tool to give you feedback on where the current perf problems lie.
And in this video, +Colt McAnlis walks you through an on-device tool that’s built for this exact reason. “Profile GPU Rendering” will help you understand the stages of the rendering pipeline, and also get a chance to see what portions of it might be taking too long, and what you can do about it for your application.
渲染性能问题往往是偷取你宝贵帧数的罪魁祸首,这种问题很容易产生,很容易出现,而且在一个非常方便的工具的帮助下,也非常容易去追踪. 使用Peofile GPU Rendering tool,你可以在手机上就可以看到究竟是什么导致你的应用程序出现卡顿,变慢的情况.
这个工具在设置-开发者选项-Profile GPU rendering选项,打开后选择on screen as bars:
然后手机屏幕上就会出现三个颜色组成的小柱状图,以及一条绿线:
这个工具会在屏幕上显示经过分析后的图形数据,最底部的图显示的是Navigation的相关信息,最上面显示的是Notification的相关信息,中间的图显示的是当前应用程序的图.
当你的应用程序在运行时,你会看到一排柱状图在屏幕上,从左到右动态地显示,每一个垂直的柱状图代表一帧的渲染,越长的垂直柱状图表示这一帧需要渲染的时间越长.随着需要渲染的帧数越来越多,他们会堆积在一起,这样你就可以观察到这段时间帧率的变化.
下图中的绿线代表16ms,要确保一秒内打到60fps,你需要确保这些帧的每一条线都在绿色的16ms标记线之下.任何时候你看到一个竖线超过了绿色的标记现,你就会看到你的动画有卡顿现象产生.
每一条柱状图都由三种颜色组成: 蓝-红-黄. 这些线直接和Android的渲染流水线和他实际运行帧数的时间关联:
蓝色代表测量绘制的时间,或者说它代表需要多长时间去创建和更新你的DisplayList.在Android中,一个视图在可以实际的进行渲染之前,它必须被转换成GPU所熟悉的格式,简单来说就是几条绘图命令,复杂点的可能是你的自定义的View嵌入了自定义的Path. 一旦完成,结果会作为一个DisplayList对象被系统送入缓存,蓝色就是记录了需要花费多长时间在屏幕上更新视图(说白了就是执行每一个View的onDraw方法,创建或者更新每一个View的Display List对象).
当你看到蓝色的线很高的时候,有可能是因为你的一堆视图突然变得无效了(即需要重新绘制),或者你的几个自定义视图的onDraw函数过于复杂.
红色代表执行的时间,这部分是Android进行2D渲染 Display List的时间,为了绘制到屏幕上,Android需要使用OpenGl ES的API接口来绘制Display List.这些API有效地将数据发送到GPU,最总在屏幕上显示出来.
记住绘制下图这样自定义的比较复杂的视图时,需要用到的OpenGl的绘制命令也会更复杂
当你看到红色的线非常高的时候,这些复杂的自定义View就是罪魁祸首:
值得一提的是,上面图中红色线较高的一种可能性是因为重新提交了视图而导致的.这些视图并不是失效的视图,但是有些时候发生了某些事,例如视图旋转,我们需要重新清理这个区域的视图,这样可能会影响这个视图下面的视图,因为这些视图都需要进行重新的绘制操作.
橙色部分表示的是处理时间,或者说是CPU告诉GPU渲染一帧的地方,这是一个阻塞调用,因为CPU会一直等待GPU发出接到命令的回复,如果柱状图很高,那就意味着你给GPU太多的工作,太多的负责视图需要OpenGL命令去绘制和处理.
保持动画流畅的关键就在于让这些垂直的柱状条尽可能地保持在绿线下面,任何时候超过绿线,你就有可能丢失一帧的内容.
GPU Profile工具能够很好地帮助你找到渲染相关的问题,但是要修复这些问题就不是那么简单了. 你需要结合代码来具体分析,找到性能的瓶颈,并进行优化.
有时候你可以以这个为工具,让负责设计这个产品的人修改他的设计,以获得良好的用户体验.
keep calm, profile your code, and always remember, Perf Matters
]]>一个人可以走的更快 , 一群人可以走的更远
Unbeknown to most developers, there’s a simple hardware design that defines everything about how fast your application can draw things to the screen.
You may have heard the term VSYNC - VSYNC stands for vertical synchronization and it’s an event that happens every time your screen starts to refresh the content it wants to show you.
Effectively, VSYNC is the product of two components Refresh Rate (how fast the hardware can refresh the screen), and Frames Per Second (how fast the GPU can draw images), and in this video +Colt McAnlis walks through each of these topics, and discusses where VSYNC (and the 16ms rendering barrier) comes from, and why it’s critical to understand if you want a silky smooth application.
想要开发一个高性能的应用程序,首先你得了解他的硬件工作原理,那么最好的办法就是去使用它,应用程序运行速度的快慢,很容易被人误解为硬件进程的控制问题,然而这最主要的根源在于渲染性能.如果你想要提高你应用程序的渲染性能,你就必须知道什么是VSYNC.
在了解VSYNC之前,我们需要了解两个概念:
刷新率代表屏幕在一秒内刷新屏幕的次数,这个值用赫兹来表示,取决于硬件的固定参数. 这个值一般是60Hz,即每16.66ms刷新一次屏幕.
帧速率代表了GPU在一秒内绘制操作的帧数,比如30fps/60fps.在这种情况下,高点的帧速率总是好的.
刷新率和帧速率需要协同工作,才能让你的应用程序的内容显示到屏幕上,GPU会获取图像数据进行绘制,然后硬件负责把内容呈现到屏幕上,这将在你的应用程序的生命周期中周而复始地发生.下面的图中每一个竖行代表了一帧的绘制和呈现工作.
不幸的是,刷新率和帧速率并不是总能够保持相同的节奏:
如果帧速率实际上比刷新率快,那么就会出现一些视觉上的问题,下面的图中可以看到,当帧速率在100fps而刷新率只有75Hz的时候,GPU所渲染的图像并非全都被显示出来.
举个例子, 你拍了一张照片,然后旋转5度再拍一张照片, 将两种图片的中间剪开并拼接在一起:
这两张图有相似之处,但是上面和下面部分有明显的区别,这就叫Tearing(撕裂),是刷新率和帧速率不一致的结果.
上面的原因是因为,当你的显卡正在使用,一个内存区正在写入帧数据(用来显示一帧的一个Buffer),从顶部开始, 新的一帧覆盖前一帧,并立刻输出一行内容. 现在,当屏幕开始刷新时,实际上并不知道缓冲区是什么状态(即不知道缓冲区中的一帧是否绘制完毕,即存在只绘制了一半的情况,另一半还是之前的那帧),因此它从GPU中抓住的帧肯可能并不是完全完整的.
目前Android的双缓冲(或者三缓冲/四缓冲), 这是非常有效的,当GPU将一帧写入一个被成为后缓冲的存储器, 而存储器中的次级区域被称为帧缓冲,当写入下一帧时,它会开始填充后缓冲,而帧缓冲保持不变,现在我们刷新屏幕,它将使用帧缓冲(事先已经绘制好),而不是正在处于绘制状态的后缓冲, 这就是VSYNC的作用.如果在屏幕刷新中,VSYNC,即垂直同步,将会在让从后缓冲到帧缓冲的拷贝过程保持同样的复制操作:
GPU的频率比屏幕刷新率高是正常的,因为你的GPU刷新会比屏幕刷新快,在这种情况下,当屏幕刷新成功,你的GPU将会等待VSYNC信号,直到下一个VSYNC信号到来时(即屏幕刷新时),这时你的帧速率就可以达到设备的刷新率上限. 当然这只是理想情况, 当fps达到60的时候,GPU需要在16.66ms内准备好一帧,这对应用程序的要求是非常高的.更不用说100fps了…
屏幕刷新率比帧速率快的情况
如果屏幕刷新率比帧速率快,屏幕会在两帧中显示同一个画面,当这种断断续续的情况发生时,你就遇到麻烦了.比如你的帧速率比屏幕刷新率高的时候,用户看到的是非常流畅的画面, 但是帧速率降下来的时候(GPU绘制太多东西的时候),用户将会很明显地察觉到动画卡住了或者掉帧,然后又恢复了流畅.这通常会被描述为闪屏, 跳帧,延迟.
你的应用程序应该避免这些帧率突降的情况.以确保GPU迅速获取数据,并在屏幕再次刷新之前写录内容.
keep calm, profile your code, and always remember, Perf Matters
]]>一个人可以走的更快 , 一群人可以走的更远
One of the most problematic performance problems on Android is the easiest to create; thankfully, it’s also easy to fix.
OVERDRAW is a term used to describe how many times a pixel has been re-drawn in a single frame of rendering. It’s a troublesome issue, because in most cases, pixels that are overdrawn do not end up contributing to the final rendered image. As such, it amounts to wasted work for your GPU and CPU.
Fixing overdraw has everything to do with using the available on-device tools, like Show GPU Overdraw, and then adjusting your view hierarchy in order to reduce areas where it may be occurring.
视频开头作者举了一个例子,说如果你是一个粉刷匠,你应该会知道,给墙壁粉刷是一件工作量非常大的工作,而且如果你需要重新粉刷一遍的话(比如对颜色不满意),那么第一次的粉刷就白干了. 同样的道理,如果你的应用程序中出现了过度绘制问题,那么你之前所做的事情也就白费了.如果你想兼顾高性能和完美的设计,那么你的程序可能会出现一个性能问题:OverDraw!
OverDraw是一个术语, 它表示某些组件在屏幕上的一个像素点的绘制超过1次.如下面的图所示,我们有一堆重叠的卡片,被用户激活的卡片在最上面,而那些没有激活的卡片在下面,这意味着我们画大力气绘制的那些卡片,基本都是不可见的.问题就在于次,我们像素渲染的并不全是用户最后能看打的部分, 这是在浪费GPU的时间!
目前流行的一些布局是一把双刃剑,带给我们漂亮的画面的同时,也带来了很大的麻烦.为了最大限度地提高应用程序的性能,你得减少OverDraw!
Android手机中提供了查看OverDraw情况的工具,在设置-开发者选项中,找到打开”Show GPU OverDraw”按钮即可:
Android会使用不同深浅的颜色来表示OverDraw的程序,没有OverDraw的时候, 你看到的是它本来的颜色,其他颜色表示不同的过度绘制程序:
你的应用程序的目标应该是尽可能地减少过度绘制,即更多的蓝色色块而不是红色色块:
虽然OverDraw很大程序上来自于你的视图互相重叠的问题,但是各位开发者更需要注意的是不必要的背景重叠.
比如在一个应用程序中,你的所有的View都有背景的话,就会看起来像第一张图中那样,而在去除这些不必要的背景之后(指的是Window的默认背景,Layout的背景,文字以及图片的可能存在的背景),效果就像第二张图那样,基本没有过度绘制的情况.
比如去除Window的默认背景:
1 | this.getWindow().setBackgroundDrawableResource(android.R.color.transparent); |
keep calm, profile your code, and always remember, Perf Matters
关于过度绘制及其优化,我博客有两篇文章专门介绍:
]]>一个人可以走的更快 , 一群人可以走的更远
Rendering performance is all about how fast you can draw your activity, and get it updated on the screen. Success here means your users feeling like your application is smooth and responsive, which means that you’ve got to get all your logic completed, and all your rendering done in 16ms or less, each and every frame. But that might be a bit more difficult than you think.
In this video, +Colt McAnlis takes a look at what “rendering performance” means to developers, alongside some of the most common pitfalls that are ran into; and let’s not forget the important stuff: the tools that help you track down, and fix these issues before they become large problems.
当你觉得自己开发了一个改变世界的应用的时候,你的用户可能并不会这么认为,他们认为你的应用又慢又卡,达不到他们所期望的那种顺滑,更谈不上改变这该死的世界了,回收站走你!等等!明明我这个应用在我的Nexus5上非常顺滑啊?你咋能说又慢又卡呢?如果你对Android的碎片化有一定了解的话,你就应该知道,很多低配置的手机并不像Nexus5那样有强大的处理器和GPU,以及没有被怎么污染的原生系统。
如果有大量的用户投诉说你的应用又卡又慢的时候,不要总是抱怨用户的低端手机,有时候问题就出在你的应用本身,也就意味着你的Android存在比较严重的渲染性能问题。只有真正了解问题发生的根源,才能有效的解决问题。所以了解Android渲染相关的知识,是一个Android开发者必不可少的知识。
渲染问题是你建立一个应用程序是最经常碰到的问题,一方面,设计师希望展现给用户一个超自然的体验,另一方面,这些华丽的动画和视图并不能在所有的Android手机上都流畅地运行。所以这就是问题所在。
Android系统每16ms都会重新绘制一次你的Activity,也就是说,你的逻辑控制画面更新要保证最多16ms一帧才能每秒达到60帧(至于为什么是60帧,这个后面会有一个专题来讲解这个)。如下图,每一帧都在16ms内绘制完成,此时的世界是顺滑的。
但是如果你的应用程序没有在16ms内完成这一帧的绘制,假设你花费了24ms来绘制这一帧,那么就会出现我们称之为掉帧的情况,世界变得有延迟了。如下图:
系统准备将新的一帧绘制到屏幕上,但是这一帧并没有准备好,所有就不会有绘制操作,画面也就不会刷新。反馈到用户身上,就是用户盯着同一张图看了32ms而不是16ms,也就是说掉帧发生了。
掉帧是用户体验中一个非常核心的问题,用户将很容易察觉到由于掉帧而产生的卡顿感,如果此时用户正在与系统进行交互,比如滑动列表,或者正在打字,那么卡顿感是非常明显的。用户会马上对你的应用进行吐槽,下一步工作肯定是回收站走你!所以弄清楚掉帧的原因是非常重要的。
不过蛋疼的是,引起掉帧发生的原因非常多,比如:
检测和解决这些问题很大程度上依赖于你的应用程序架构,但是幸运的是,我们有很多开发者工具来协助我们发现和解决这些问题,有些工具甚至能追踪到具体出错的代码行数或者UI控件,这些工具包括但不限于:
你可以使用Hierarchy View 来查看你的View是否过于复杂,如果是,那么说明你有很多时间没有利用。并且浪费了许多时间进行重绘。
Hierarchy View 位于Android Device Monitor 中,Android Device Monitor在Eclipse和Android Studio中都有有对应的入口,依次选则Window-Open Perspective-Hierarchy View即可打开Hierarchy View视图。 Hierarchy View视图虽然比较简单,但是非常有效。花费一点了解这个工具每一个细节,对于以后排查问题来说都是事半功倍。关于Hierarchy View视图的用法,会有更详细的单独的教程来讲解。
这三个选项在设置-辅助功能- 开发者选项中,默认都是关闭的。Profile GPU Rendering 和 GPU Overdraw比较重要,所以系列视频后面会有专门的专题会讲解,这里简单介绍一下GPU View Updates。GPU View Updates的作用是使用GPU进行绘图时闪烁显示窗口中的视图。随着android版本的更新,越来越多的绘制操作能使用GPU来完成,详见http://developer.android.com/guide/topics/graphics/hardware-accel.html,而这个工具打开之后,使用GPU绘制的区域会用红色来标注,而没有红色标注的区域,则是使用CPU绘制的。这个选项也可以用来查看redraw的区域大小。
TraceView是一个很棒的检查是否掉帧的工具,视频中没有对此工具进行介绍,但是这个工具非常的重要,他可以找到你代码中花费时间的地方,精确到每一个函数,不论这个函数是你应用程序中的还是系统函数。另外在Android Studio中,TraceView得到了改进,其视图能非常直观的显示出每一帧所消耗的时间,函数像倒金字塔一般展现在面前,我们可以很容易地看出掉帧的地方以及那一帧里面所有的函数调用情况。鉴于此工具非常实用,所有会有更详细的单独的教程来讲解。
keep calm, profile your code, and always remember, Perf Matters
这是这个系列视频的第一个视频,从内容上来看,是从一个大的角度来看Render Performance,简单地讲述了一下Render Performance基本的概念,出现的原因以及排查的工具。在发现问题–定位问题–解决问题的流程上属于发现问题–定位问题,解决问题则基本没有提到。这也基本符合这一系列视频的基调:即着重于发现问题(使用工具发现问题、挖掘问题出现的原理和原因)和定位问题(使用工具定位),如何解决问题则需要自己通过实战去进行锻炼,毕竟这种问题并没有一个通用的解决方法,每个应用都有每个应用自己的问题。
]]>一个人可以走的更快 , 一群人可以走的更远
2015年1月6日,Google官方发布了一系列关于Android性能优化的小视频,将其命名为Android Performance Patterns,这一些列视频放在YouTube上,观看的话需要科学地上网。
官方简介:
Android Performance Patterns is a collection of videos focused entirely on helping developers write faster, more performant Android Applications. On one side, it’s about peeling back the layers of the Android System, and exposing how things are working under the hood. On the other side, it’s about teaching you how the tools work, and what to look for in order to extract the right perf out of your app.
But at the end of the day, Android Performance Patterns is all giving you the right resources, at the right time to help make the fastest, smoothest, most awesome experience for your users. And that’s the whole point, right?
总之就是一系列讲解Android性能相关的视频。这些小视频的时间非常短,在3-5分钟之内,主讲人的英文语速也非常快,初期这些视频没有翻译的时候,着实考验了一把听力。好消息是现在这些视频已经都有中英文字幕了。
这些视频的时间虽然很短,但是信息量却非常大,有些他一句话带过的内容,我们却需要花费很多的时间去研究他的原理,或者研究一个调试工具如何使用。也就是说,这一系列视频并没有真正教你如何去优化你的应用,而是告诉你关于Android性能优化你需要知道的知识,这样你去优化你的Android应用的时候,知道该用什么工具,该采取什么样的步骤,需要达到什么样的目标。
由于我最近也在研究Android性能优化相关的课题,这个视频第一时间出来的时候我就看了好几遍,所以一早就有将这一系列视频翻译成中文的想法。后来看了几遍之后,我发现仅仅翻译成中文其实意义不大,他所讲述的每一个知识点,都是可以写成一篇博文甚至好几篇博文的,所以就有了这一系列文章的出现。
每一篇文章中,我都会先将视频中涉及到的知识点列出来,然后一一进行讲解。有些调试工具由于篇幅原因,可能会写到单独的博文中。
另外催生我写这一系列文章的是胡凯,他的博客 http://hukai.me/android-performance-patterns 第一时间就将这一些列视频的内容翻译成了中文,优美的排版加上过硬的翻译,让这篇博文被广泛传播,备受好评。同时他也是github上 android-training-course-in-chinese 项目的发起人,他的Github主页:https://github.com/kesenhoo。他对于分享的热情我非常敬佩。如果你并非是Android应用开发者或者对技术细节不感兴趣的话,直接看他的那篇Android性能优化典范即可,看完之后你会对这一些列视频有一个大概的认识。
下面是关于Android性能优化典范这一些列视频的一些资源信息:
OK,Let us start!!!
]]>一个人可以走的更快 , 一群人可以走的更远
在使用MAT查看应用程序内存使用情况的时候,我们经常会碰到Bitmap对象以及BitmapDrawable$BitmapState对象,而且在内存使用上,Bitmap所占用的内存占大多数.在这样的情况下, Bitmap所造成的内存泄露尤其严重, 需要及时发现并且及时处理.在这样的需求下, 当我们在MAT中发现和图片相关的内存泄露的时候, 如果能知道是那一张图片,对分析问题会有很大的帮助.
本文就介绍如何将MAT中的Bitmap数组对象还原成一张图片。
Bitmap对象如图:
在MAT中打开Dominator Tree视图 , 选择一个Bitmap对象 , 查看此时右边的Inspector窗口,内容如下图:
这个视图中,可以看到这个Bitmap的一些基本的信息: mBuffer, mHeight, mWidth , mNativeBitmap等, 宽和高的值我们一会需要用的到 .
mBuffer的定义在Bitmap.java中:
1 | /** |
其值是保存在byte数组中的, 我们需要的就是这个byte数组中的内容. 在Inspector窗口的mBuffer这一栏或者Dominator Tree视图的Bitmap这一栏点开下一级,都可以看到这个byte数组的内容. 鼠标右键选择Copy –>Save Value To File. 弹出如下对话框:
选择存储路径和文件名,这里需要注意的是,文件名一定要以 .data为后缀,否则无法正常使用,切记.
这时需要借助Linux上强大的图片应用:GIMP,没安装的可以去安装一下. 安装好之后, 打开GIMP,选择文件-打开.选择我们上一步导出的.data文件(比如image.data),然后会出现如下图的属性框:
图像类型这一栏选择RGB Alpha, 宽度和高度必填, 其值可以在MAT中查看到,第一步的时候有说到这个值的位置, 其他的选择默认即可,然后点击打开. GIMP就会把这个文件打开.
Mac和Windows可以选择使用PhotoShop作为打开的工具, 和Linux唯一不同的地方在于. 保存的文件的格式需要以.raw结尾 (比如image.raw),选择深度为32位. 其余的和Linux相同.
另外GIMP也有Mac、Windows版本,建议大家在各个平台都使用GIMP,这样学习成本比较低,而且GIMP为免费软件,使用起来功能也非常多。
]]>一个人可以走的更快 , 一群人可以走的更远
Android Studio的最新版本可以直接获取hprof文件:
然后选择文件,点击右键转换成标准的hprof文件,就可以在MAT中打开了。
在使用使用Eclipse或者AndroidStudio抓内存之前,一定要手动点击 Initiate GC按钮手动触发GC,这样抓到的内存使用情况就是不包括Unreachable对象的。
Unreachable指的是可以被垃圾回收器回收的对象,但是由于没有GC发生,所以没有释放,这时抓的内存使用中的Unreachable就是这些对象。
点击Calculate Retained Size之后,会出现Retained Size这一列
可以看到Unreachable Object的对象其Retained Heap值都为0.这也是正常的。
MAT中Histogram的主要作用是查看一个instance的数量,一般用来查看自己创建的类的实例的个数。
通过查看Object的个数,结合代码就可以找出存在内存泄露的类(即可达但是无用的对象,或者是可以重用但是重新创建的对象)
Histogram中还可以对对象进行Group,更方便查看自己Package中的对象信息。
MAT中可以查看当前的Thread信息:
从图中可以得到的信息:
MAT中的各个视图中,在每一个Item中点击右键会出现很多选项,很多时候我们需要依赖这些选项来进行分析:
这些选项的具体含义则可以通过右键中的Search Queries这个选项(上图中的倒数第四个选项)进行搜索和查看,非常的有用。
可以看到,所有的命令其实就是配置不同的SQL查询语句。
比如我们最常用的:
如果经常使用MAT分析内存,就会发现Bitmap所占用的内存是非常大的,这个和其实际显示面积是有关系的。在2K屏幕上,一张Bitmap能达到20MB的大小。
所以要是MAT提供了一种方法,可以将存储Bitmap的byte数组导出来,使用第三方工具打开。这个大大提高了我们分析内存泄露的效率。
关于这个方法的操作流程,可以参考这篇文章还原MAT中的Bitmap图像.
I
ArrayList是使用非常常用的一个数据结构,在MAT中,如果想知道ArrayList中有哪些数据,需要右键-> List Objects -> With outgoing references,然后可以看到下面的图:
从上图可以看到,这个ArrayList的内容在一个array数组中,即暴漏了ArrayList的内部结构,查看的时候有点不方便,所以MAT提供了另外一种查看ArrayList内数据的方式:
其结果非常直观:
Big Drops In Dominator Tree选项在右键->
Displays memory accumulation points in the dominator tree. Displayed are objects with a big difference between the retained size of the parent and the children and the first “interesting” dominator of the accumulation point. These are places where the memory of many small objects is accumulated under one object.
]]>一个人可以走的更快 , 一群人可以走的更远
MAT(Memory Analyzer Tool),一个基于 Eclipse 的内存分析工具,是一个快速、功能丰富的JAVA heap分析工具,它可以帮助我们查找内存泄漏和减少内存消耗。使用内存分析工具从众多的对象中进行分析,快速的计算出在内存中对象的占用大小,看看是谁阻止了垃圾收集器的回收工作,并可以通过报表直观的查看到可能造成这种结果的对象。
当然MAT也有独立的不依赖Eclipse的版本,只不过这个版本在调试Android内存的时候,需要将DDMS生成的文件进行转换,才可以在独立版本的MAT上打开。不过Android SDK中已经提供了这个Tools,所以使用起来也是很方便的。
这里是MAT的下载地址:[https://eclipse.org/mat/downloads.php](https://eclipse.org/mat/ downloads.php),下载时会提供三种选择的方式:
下载安装好之后,就可以使用MAT进行实际的操作了。
使用MAT工具之前,要对Android的内存分配方式有基本的了解,对容易引起内存泄露的代码也要保持敏感,在代码级别对内存泄露的排查,有助于内存的使用。
Android主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存是比较有限的。如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使得Android应用程序安全且快速的运行,Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。一方面,如果程序在运行过程中出现了内存泄漏的问题,仅仅会使得自己的进程被kill掉,而不会影响其他进程(如果是system_process等系统进程出问题的话,则会引起系统重启)。另一方面Android为不同类型的进程分配了不同的内存使用上限,如果应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉。
描述:
程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。
示例代码:
1 | Cursor cursor = getContentResolver().query(uri ...); |
修正示例代码:
1 | Cursor cursor = null; |
`
描述:以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:
1 | public View getView(int position, View convertView, ViewGroup parent) |
来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参 View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。
由此可以看出,如果我们不去使用convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。ListView回收list item的view对象的过程可以查看:android.widget.AbsListView.java –> void addScrapView(View scrap) 方法。
示例代码:
1 | public View getView(int position, View convertView, ViewGroup parent) { |
`
示例修正代码:
1 | public View getView(int position, View convertView, ViewGroup parent) { |
关于ListView的使用和优化,可以参考这两篇文章:
描述:有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存。
另外在最新版本的Android开发时,使用下面的方法也可以释放此Bitmap所占用的内存
1 | Bitmap bitmap ; |
描述:这种情况描述起来比较麻烦,举两个例子进行说明。
示例A:
假设有如下操作
1 | public class DemoActivity extends Activity { |
我们有一个成员变量 obj,在operation()中我们希望能够将处理obj实例的操作post到某个线程的MessageQueue中。在以上的代码中,即便是mHandler所在的线程使用完了obj所引用的对象,但这个对象仍然不会被垃圾回收掉,因为DemoActivity.obj还保有这个对象的引用。所以如果在DemoActivity中不再使用这个对象了,可以在[Mark]的位置释放对象的引用,而代码可以修改为:
1 | public void operation() { |
示例B:
假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。
但是如果在释放LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process进程挂掉。
总之当一个生命周期较短的对象A,被一个生命周期较长的对象B保有其引用的情况下,在A的生命周期结束时,要在B中清除掉对A的引用。
Android应用程序中最典型的需要注意释放资源的情况是在Activity的生命周期中,在onPause()、onStop()、onDestroy()方法中需要适当的释放资源的情况。由于此情况很基础,在此不详细说明,具体可以查看官方文档对Activity生命周期的介绍,以明确何时应该释放哪些资源。
另外一些其他的例子,将会在补充版本加入。
HPROF文件是MAT能识别的文件,HPROF文件存储的是特定时间点,java进程的内存快照。有不同的格式来存储这些数据,总的来说包含了快照被触发时java对象和类在heap中的情况。由于快照只是一瞬间的事情,所以heap dump中无法包含一个对象在何时、何地(哪个方法中)被分配这样的信息。
这个文件可以使用DDMS导出:
选择存储路径保存后就可以得到对应进程的HPROF文件。eclipse插件可以把上面的工作一键完成。只需要点击Dump HPROF file图标,然后MAT插件就会自动转换格式,并且在eclipse中打开分析结果。eclipse中还专门有个Memory Analysis视图
1 | hprof-conv xxx.xxx.xxx.hprof xxx.xxx.xxx.hprof |
转换过后的.hprof文件即可使用MAT工具打开了。
这里介绍的不是MAT这个工具的主界面,而是导入一个文件之后,显示OverView的界面。
如果选择了第一个,则会生成一个报告。这个无大碍。
选择OverView界面:
我们需要关注的是下面的Actions区域
Histogram:列出内存中的对象,对象的个数以及大小
Dominator Tree:列出最大的对象以及其依赖存活的Object (大小是以Retained Heap为标准排序的)
Top Consumers : 通过图形列出最大的object
Duplicate Class:通过MAT自动分析泄漏的原因
一般Histogram和 Dominator Tree是最常用的。
要看懂MAT的列表信息,Shallow heap、Retained Heap、GC Root这几个概念一定要弄懂。
Shallow size就是对象本身占用内存的大小,不包含其引用的对象。
因为不像c++的对象本身可以存放大量内存,java的对象成员都是些引用。真正的内存都在堆上,看起来是一堆原生的byte[], char[], int[],所以我们如果只看对象本身的内存,那么数量都很小。所以我们看到Histogram图是以Shallow size进行排序的,排在第一位第二位的是byte,char 。
Retained Heap的概念,它表示如果一个对象被释放掉,那会因为该对象的释放而减少引用进而被释放的所有的对象(包括被递归释放的)所占用的heap大小。于是,如果一个对象的某个成员new了一大块int数组,那这个int数组也可以计算到这个对象中。相对于shallow heap,Retained heap可以更精确的反映一个对象实际占用的大小(因为如果该对象释放,retained heap都可以被释放)。
这里要说一下的是,Retained Heap并不总是那么有效。例如我在A里new了一块内存,赋值给A的一个成员变量。此时我让B也指向这块内存。此时,因为A和B都引用到这块内存,所以A释放时,该内存不会被释放。所以这块内存不会被计算到A或者B的Retained Heap中。为了纠正这点,MAT中的Leading Object(例如A或者B)不一定只是一个对象,也可以是多个对象。此时,(A, B)这个组合的Retained Set就包含那块大内存了。对应到MAT的UI中,在Histogram中,可以选择Group By class, superclass or package来选择这个组。
为了计算Retained Memory,MAT引入了Dominator Tree。加入对象A引用B和C,B和C又都引用到D(一个菱形)。此时要计算Retained Memory,A的包括A本身和B,C,D。B和C因为共同引用D,所以他俩的Retained Memory都只是他们本身。D当然也只是自己。我觉得是为了加快计算的速度,MAT改变了对象引用图,而转换成一个对象引用树。在这里例子中,树根是A,而B,C,D是他的三个儿子。B,C,D不再有相互关系。把引用图变成引用树,计算Retained Heap就会非常方便,显示也非常方便。对应到MAT UI上,在dominator tree这个view中,显示了每个对象的shallow heap和retained heap。然后可以以该节点位树根,一步步的细化看看retained heap到底是用在什么地方了。要说一下的是,这种从图到树的转换确实方便了内存分析,但有时候会让人有些疑惑。本来对象B是对象A的一个成员,但因为B还被C引用,所以B在树中并不在A下面,而很可能是平级。
为了纠正这点,MAT中点击右键,可以List objects中选择with outgoing references和with incoming references。这是个真正的引用图的概念,
为了更好地理解Retained Heap,下面引用一个例子来说明:
把内存中的对象看成下图中的节点,并且对象和对象之间互相引用。这里有一个特殊的节点GC Roots,这就是reference chain(引用链)的起点:
从obj1入手,上图中蓝色节点代表仅仅只有通过obj1才能直接或间接访问的对象。因为可以通过GC Roots访问,所以左图的obj3不是蓝色节点;而在右图却是蓝色,因为它已经被包含在retained集合内。
所以对于左图,obj1的retained size是obj1、obj2、obj4的shallow size总和;
右图的retained size是obj1、obj2、obj3、obj4的shallow size总和。
obj2的retained size可以通过相同的方式计算。
GC发现通过任何reference chain(引用链)无法访问某个对象的时候,该对象即被回收。名词GC Roots正是分析这一过程的起点,例如JVM自己确保了对象的可到达性(那么JVM就是GC Roots),所以GC Roots就是这样在内存中保持对象可到达性的,一旦不可到达,即被回收。通常GC Roots是一个在current thread(当前线程)的call stack(调用栈)上的对象(例如方法参数和局部变量),或者是线程自身或者是system class loader(系统类加载器)加载的类以及native code(本地代码)保留的活动对象。所以GC Roots是分析对象为何还存活于内存中的利器。
Thread OvewView可以查看这个应用的Thread信息:
在Histogram和Domiantor Tree界面,可以选择将结果用另一种Group的方式显示(默认是Group by Object),切换到Group by package,可以更好地查看具体是哪个包里的类占用内存大,也很容易定位到自己的应用程序。
在Histogram或者Domiantor Tree的某一个条目上,右键可以查看其GC Root Path:
这里也要说明一下Java的引用规则:
从最强到最弱,不同的引用(可到达性)级别反映了对象的生命周期。
点击Path To GC Roots –> with all references
]]>一个人可以走的更快 , 一群人可以走的更远
本文是一篇译文,原文Android Performance Case Study Follow-up的作者是大名鼎鼎的Romain Guy。本文讲述了Android性能优化的一些技巧、方法和工具。
两年前,我发表了一篇名为Android Performance Case Study 的文章,来帮助Android开发者了解需要使用什么工具和技术手段来确定、追踪和优化性能问题。
那篇文章以一个Twitter客户端 Falcon Pro为典范,其开发人员为 Joaquim Vergès. Joaquim人不错,他允许我在我的文章中使用它的程序作为例子,并且快速处理了我发现的所有问题。一切都OK,直到Joaquim 从头开始开发Falcon Pro 3,前不久在他准备发布它的新应用的时候,他联系了我,因为他有一个和滚动相关的性能问题需要我来帮助他,这一次我依然没有源代码可以参考。
Joaquim使用了所有的工具来找出问题所在,他发现Overdraw不是问题的原因,他觉得是 ViewPager 的用法导致了这个问题。他给我发来了下面的截图:
Joaquim使用了系统内置的GPU profiling工具来发现掉帧现象, 左边的截图是在没有ViewPager 的情况下滑动时间线,右边的截图是有ViewPager的情况下滑动(他使用的是2014年的Moto x来截的图),问题看起来很明显。
我最先想到的是查看ViewPager是不是由于滥用硬件加速导致,这个性能问题看起来像是在滑动的过程中每一帧都使用了硬件加速。系统的 hardware layers updates debugging tool没有显示什么有用的信息。我反复使用HierarchyViewer 查看布局情况,令我满意的是ViewPager的表现很正确(相反,不太可能会出问题)
之后我打开了另一个强大的工具却很少用到的工具:Tracer for OpenGL 。我之前的那篇文章解释了如何使用工具获得更多细节。你首先需要知道的是这个工具收集了所有UI界面发给GPU的绘制命令。
Android 4.3 and up: Tracer has unfortunately become a little more difficult to use since Android 4.3 when we introducedreordering and merging of drawing commands. It’s an amazingly useful optimization but it prevents Tracer from grouping drawing commands by view. You can restore the old behavior by disabling display lists optimization using the following command (before you start your application)(意思是说Android4.3之后,这个工具不太好用了,因为有reordering and merging 机制的引进)
Reading OpenGL traces: Commands shown in blue are GL operations that draw pixels on screen. All other commands are used to transfer data or set state and can easily be ignored. Every time you click on one of the blue commands, Tracer will update the Details tab and show you the content of the current render target right after the command you clicked is executed. You can thus reconstruct a frame by clicking on each blue command one after another. It’s pretty much how I analyze performance issues with Tracer. Seeing how a frame is rendered gives a lot of insight on what the application is doing.(意思是说只蓝色的行是真正进行绘制的命令,点击可以看到绘制的这一帧的图像,其他的命令都是一些数据的转换)
滑动一段时间Falcon Pro应用后,我仔细查看Gl Trace收集到的数据,我很惊奇地发现很多SaveLayer/ComposeLayer阻塞命令。
这些命令表明应用在生成一个临时的Hardware Layer。这些临时的Layer被不同的 [Canvas.saveLayer()](http://developer.android.com/reference/android/graphics/Canvas.html#saveLayer(float, float, float, float, android.graphics.Paint, int))所创建,这些UI控件在下面的情况下使用Canvas.saveLayer()方法去绘制 alpha < 1 (seeView.setAlpha()的View(即半透明View):
我和Chet 在很多演示中解释过为什么你应该 use alpha with care,每次UI控件使用一个临时的Layer,绘制命令会发送不同的渲染目标,对GPU来说,切换渲染目标是很昂贵的操作,这对于使用tiling/deferred架构的GPU(ImaginationTech’s SGX, Qualcomm’s Adreno, etc)等是硬伤,直接渲染架构的GPU,比如 Nvidia,则会好一点。因为我和Joaquim 使用的是搭载高通处理器的Moto X 2014版本,所以使用多个临时硬件层是最有可能的性能问题的根源。
那么问题来了,是什么创建了这些临时的Layer呢?Tracer告诉我们了答案,如果你看了刚刚上面那张图,你可以看到只有SaveLayer这个组中OpenGl命令绘制了一个小圆圈(图被工具放大了),我们来看一下应用截图:
你看到最上面的小圆圈了么?那是ViewPager的指示器,来显示当前的位置。Joaquim 使用了一个第三方库来绘制这些指示器,有趣的是这些库如何绘制指示器的:当前的Page用一个白色的圈指示,其他的页用类似灰色的圆圈来指示。我说类似灰色因为这个圆圈其实是半透明的白色圆圈。这个库使用 setAlpha()方法来给每个圆圈设置颜色。
有下面几种方法来解决这个问题:
Use a customizable “inactive” color instead of setting an opacity on the View( 使用动态的“inactive”颜色(即根据状态来设置View的颜色)而不是设置透明度。)
Return false from hasOverlappingRendering() and the framework will set the proper alpha on the Paint
for you(使hasOverlappingRendering()返回false,这样系统会设置适当的alpha,关于这个的用法,这篇文章中有提到:同时Android提供了hasOverlappingRendering()接口,通过重写该接口可以告知系统当前View是否存在内容重叠的情况,帮助系统优化绘制流程,原理是这样的:对于有重叠内容的View,系统简单粗暴的使用 offscreen buffer来协助处理。当告知系统该View无重叠内容时,系统会分别使用合适的alpha值绘制每一层。)
1 | /** |
Return true from onSetAlpha() and set an alpha on the Paint used to draw the “gray” circles(使onSetAlpha() 返回True并对Paint设置alpha来绘制“gray”圆圈)
1 | paint.setAlpha((int) alpha * 255); |
最简单的方法是使用第二种,但是他只能在API16以上使用,如果你要支持旧版本的Android,使用其他两个方法,我相信Joaquim 已经丢弃那个第三方库并使用自己的指示器了。
我希望这篇文章能让大家清楚如何从看似无辜的和无害的操作中寻找可能会出现性能问题。所以请记住:不要仅仅做出假设,要实际去验证、测量。
更多关于Alpha的使用,可以参考这篇文章:
Android Tips: Best Practices for Using Alpha
]]>一个人可以走的更快 , 一群人可以走的更远
ViewDragHelper ——视图拖动是一个比较复杂的问题。这个类可以帮助解决不少问题。如果你需要一个例子,DrawerLayout就是利用它实现扫滑。Flavient Laurent 还写了一些关于这方面的优秀文章。
PopupWindow——Android到处都在使用PopupWindow ,甚至你都没有意识到(标题导航条ActionBar,自动补全AutoComplete,编辑框错误提醒Edittext Errors)。这个类是创建浮层内容的主要方法。
Actionbar.getThemrContext()——导航栏的主题化是很复杂的(不同于Activity其他部分的主题化)。你可以得到一个上下文(Context),用这个上下文创建的自定义组件可以得到正确的主题。
ThumbnailUtils——帮助创建缩略图。通常我都是用现有的图片加载库(比如,Picasso 或者 Volley),不过这个ThumbnaiUtils可以创建视频缩略图。译者注:该API从V8才开始支持。
Context.getExternalFilesDir()———— 申请了SD卡写权限后,你可以在SD的任何地方写数据,把你的数据写在设计好的合适位置会更加有礼貌。这样数据可以及时被清理,也会有更好的用户体验。此外,Android 4.0 Kitkat中在这个文件夹下写数据是不需要权限的,每个用户有自己的独立的数据存储路径。译者注:该API从V8才开始支持。
SparseArray——Map的高效优化版本。推荐了解姐妹类SparseBooleanArray、SparseIntArray和SparseLongArray。
PackageManager.setComponentEnabledSetting()——可以用来启动或者禁用程序清单中的组件。对于关闭不需要的功能组件是非常赞的,比如关掉一个当前不用的广播接收器。
SQLiteDatabase.yieldIfContendedSafely()——让你暂时停止一个数据库事务, 这样你可以就不会占用太多的系统资源。
Environment.getExternalStoragePublicDirectory()——还是那句话,用户期望在SD卡上得到统一的用户体验。用这个方法可以获得在用户设备上放置指定类型文件(音乐、图片等)的正确目录。
View.generateViewId()——每次我都想要推荐动态生成控件的ID。需要注意的是,不要和已经存在的控件ID或者其他已经生成的控件ID重复。
ActivityManager.clearApplicationUserData()—— 一键清理你的app产生的用户数据,可能是做用户退出登录功能,有史以来最简单的方式了。
Context.createConfigurationContext() ——自定义你的配置环境信息。我通常会遇到这样的问题:强制让一部分显示在某个特定的环境下(倒不是我一直这样瞎整,说来话长,你很难理解)。用这个实现起来可以稍微简单一点。
ActivityOptions ——方便的定义两个Activity切换的动画。 使用ActivityOptionsCompat 可以很好解决旧版本的兼容问题。
AdapterViewFlipper.fyiWillBeAdvancedByHostKThx()——仅仅因为很好玩,没有其他原因。在整个安卓开源项目中(AOSP the Android ——pen Source Project Android开放源代码项目)中还有其他很有意思的东西(比如
GRAVITY_DEATH_STAR_I)。不过,都不像这个这样,这个确实有用
ViewParent.requestDisallowInterceptTouchEvent() ——Android系统触摸事件机制大多时候能够默认处理,不过有时候你需要使用这个方法来剥夺父级控件的控制权(顺便说一下,如果你想对Android触摸机制了解更多,这个演讲会令你惊叹不已。)
原文地址:http://blog.danlew.net/2014/03/30/android-tips-round-up-part-5/
原文作者:http://blog.danlew.net/about/
本文地址:https://www.androidperformance.com/android-tips-round-up-5.html 转载请注明.
]]>一个人可以走的更快 , 一群人可以走的更远
Activity.isChangingConfigurations ()——如果在 Activity 中 configuration 会经常改变的话,使用这个方法就可以不用手动做保存状态的工作了。
SearchRecentSuggestionsProvider——可以创建最近提示效果的 provider,是一个简单快速的方法。
ViewTreeObserver——这是一个很棒的工具。可以进入到 VIew 里面,并监控 View 结构的各种状态,通常我都用来做 View 的测量操作(自定义视图中经常用到)。
org.gradle.daemon=true——这句话可以帮助减少 Gradle 构建的时间,仅在命令行编译的时候用到,因为 Android Studio 已经这样使用了。
DatabaseUtils——一个包含各种数据库操作的使用工具。
android:weightSum (LinearLayout)——如果想使用 layout weights,但是却不想填充整个 LinearLayout 的话,就可以用 weightSum 来定义总的 weight 大小。
android:duplicateParentState (View)——此方法可以使得子 View 可以复制父 View 的状态。比如如果一个 ViewGroup 是可点击的,那么可以用这个方法在它被点击的时候让它的子 View 都改变状态。
android:clipChildren (ViewGroup)——如果此属性设置为不可用,那么 ViewGroup 的子 View 在绘制的时候会超出它的范围,在做动画的时候需要用到。
android:fillViewport (ScrollView)——在这片文章中有详细介绍文章链接,可以解决在 ScrollView 中当内容不足的时候填不满屏幕的问题。
android:tileMode (BitmapDrawable)——可以指定图片使用重复填充的模式。
android:enterFadeDuration/android:exitFadeDuration (Drawables)——此属性在 Drawable 具有多种状态的时候,可以定义它展示前的淡入淡出效果。
android:scaleType (ImageView)——定义在 ImageView 中怎么缩放/剪裁图片,一般用的比较多的是“centerCrop”和“centerInside”。
Merge——此标签可以在另一个布局文件中包含别的布局文件,而不用再新建一个 ViewGroup,对于自定义 ViewGroup 的时候也需要用到;可以通过载入一个带有标签的布局文件来自动定义它的子部件。
AtomicFile——通过使用备份文件进行文件的原子化操作。这个知识点之前我也写过,不过最好还是有出一个官方的版本比较好。
原文地址:http://blog.danlew.net/2014/03/30/android-tips-round-up-part-4/
原文作者:http://blog.danlew.net/about/
本文地址:https://www.androidperformance.com/android-tips-round-up-4.html 转载请注明.
]]>一个人可以走的更快 , 一群人可以走的更远
UrlQuerySanitizer——使用这个工具可以方便对 URL 进行检查。
Fragment.setArguments——因为在构建 Fragment 的时候不能加参数,所以这是个很好的东西,可以在创建 Fragment 之前设置参数(即使在 configuration 改变的时候仍然会导致销毁/重建)。
DialogFragment.setShowsDialog ()—— 这是一个很巧妙的方式,DialogFragment 可以作为正常的 Fragment 显示!这里可以让 Fragment 承担双重任务。我通常在创建 Fragment 的时候把 onCreateView ()和 onCreateDialog ()都加上,就可以创建一个具有双重目的的 Fragment。
FragmentManager.enableDebugLogging ()——在需要观察 Fragment 状态的时候会有帮助。
LocalBroadcastManager——这个会比全局的 broadcast 更加安全,简单,快速。像 otto 这样的 Event buses 机制对你的应用场景更加有用。
PhoneNumberUtils.formatNumber ()——顾名思义,这是对数字进行格式化操作的时候用的。
Region.op()——我发现在对比两个渲染之前的区域的时候很实用,如果你有两条路径,那么怎么知道它们是不是会重叠呢?使用这个方法就可以做到。
Application.registerActivityLifecycleCallbacks——虽然缺少官方文档解释,不过我想它就是注册 Activity 的生命周期的一些回调方法(顾名思义),就是一个方便的工具。
versionNameSuffix——这个 gradle 设置可以让你在基于不同构建类型的 manifest 中修改版本名这个属性,例如,如果需要在在 debug 版本中以”-SNAPSHOT”结尾,那么就可以轻松的看出当前是 debug 版还是 release 版。
CursorJoiner——如果你是只使用一个数据库的话,使用 SQL 中的 join 就可以了,但是如果收到的数据是来自两个独立的 ContentProvider,那么 CursorJoiner 就很实用了。
Genymotion——一个非常快的 Android 模拟器,本人一直在用。
-nodpi——在没有特别定义的情况下,很多修饰符(-mdpi,-hdpi,-xdpi等等)都会默认自动缩放 assets/dimensions,有时候我们需要保持显示一致,这种情况下就可以使用 -nodpi。
BroadcastRecevier.setDebugUnregister ()——又一个方便的调试工具。
Activity.recreate ()——强制让 Activity 重建。
PackageManager.checkSignatures ()——如果同时安装了两个 app 的话,可以用这个方法检查。如果不进行签名检查的话,其他人可以轻易通过使用一样的包名来模仿你的 app。
原文地址:http://blog.danlew.net/2014/03/30/android-tips-round-up-part-3/
原文作者:http://blog.danlew.net/about/
本文地址:https://www.androidperformance.com/android-tips-round-up-3.html 转载请注明.
]]>一个人可以走的更快 , 一群人可以走的更远
本文假设你已经下载了AndroidL的源码,并且有一台Nexus5手机(手机系统开发人员必备), 如果你还没有AndroidL的源码,或者你有源码但是没有配置编译的环境,那么 Initializing a Build Environment 和Downloading the Source这两篇文章你应该先去看一下(我又一次假设你会翻墙,如果你不会翻墙,那么下代码也是一个痛苦的事情). 这后面的教程Google官网也有教程.所以我只针对Nexus5进行讲解.
另外你需要知道AOSP,AOSP即Android Open Source Project 汉语意思是:谷歌开放源代码项目.我们通过Google官方下载的源代码,就是AOSP的代码, 其中是不包含Google开发的那些个应用的,各个厂商拿到的也是这个版本,在这个版本的基础上进行修改. 而Google发布的工厂固件则是包含全套Google服务的. 厂商如果想安装Google服务,就需要过Google的那一套认证,比较麻烦,而且价格不菲,鉴于Google在国内的尴尬地位,国内很多厂商都没有过这个认证.
1 | . build/envsetup.sh |
1 | lunch |
1 | You're building on Linux |
DEVICE | CODE NAME | BUILD CONFIGURATION |
---|---|---|
Nexus 6 | shamu | aosp_shamu-userdebug |
Nexus Player | fugu | aosp_fugu-userdebug |
Nexus 9 | volantis (flounder) | aosp_flounder-userdebug |
Nexus 5 (GSM/LTE) | hammerhead | aosp_hammerhead-userdebug |
Nexus 7 (Wi-Fi) | razor (flo) | aosp_flo-userdebug |
Nexus 7 (Mobile) | razorg (deb) | aosp_deb-userdebug |
Nexus 10 | mantaray (manta) | full_manta-userdebug |
Nexus 4 | occam (mako) | full_mako-userdebug |
Nexus 7 (Wi-Fi) | nakasi (grouper) | full_grouper-userdebug |
Nexus 7 (Mobile) | nakasig (tilapia) | full_tilapia-userdebug |
Galaxy Nexus (GSM/HSPA+) | yakju (maguro) | full_maguro-userdebug |
Galaxy Nexus (Verizon) | mysid (toro) | aosp_toro-userdebug |
Galaxy Nexus (Experimental) | mysidspr (toroplus) | aosp_toroplus-userdebug |
PandaBoard (Archived) | panda | aosp_panda-userdebug |
Motorola Xoom (U.S. Wi-Fi) | wingray | full_wingray-userdebug |
Nexus S | soju (crespo) | full_crespo-userdebug |
Nexus S 4G | sojus (crespo4g) | full_crespo4g-userdebug3.2 |
BUILDTYPE | USE |
---|---|
user | limited access; suited for production |
userdebug | like “user” but with root access and debuggability; preferred for debugging |
eng | development configuration with additional debugging tools |
选择aosp_hammerhead-userdebug之后,会有下面的确认信息:
1 | ============================================ |
接下来需要在Android官网下载 Nexus5所需要的驱动:
HARDWARE COMPONENT | COMPANY | DOWNLOAD | MD5 CHECKSUM | SHA-1 CHECKSUM |
---|---|---|---|---|
NFC, Bluetooth, Wi-Fi | Broadcom | Link | 2c398994e37093df51b105d63f0eb611 | 991346159c95ae75f760014a6822b8b3e8667700 |
Camera, Sensors, Audio | LG | Link | 74cf8235e6bb04da28b2ff738b13eee9 | 175dd5bae81bb54030d072cb0f0b4ec81eb3f71f |
Graphics, GSM, Camera, GPS, Sensors, Media, DSP, USB | Qualcomm | Link | 0a43395e175d3de3dc312d8abdcb4f20 | 007cf9d49f0409d5c703e7f2811fd153fee22353 |
下载完成后,解压出来是三个.sh文件,放到Android源码目录下面,然后执行.会将相关驱动放到vender目录下面.
1 | make -j8 |
如果没有出错的话,在经过漫长的时间之后,编译成功:
1 | Installed file list: out/target/product/hammerhead/installed-files.txt |
Nexus5关机状态下,长按音量下+电源,即可进入recovery模式, 然后在源码根目录下执行下面命令:
1 | fastboot -w flashall |
刷机成功后会自动重启
1 | ~/Android-SourceCode fastboot -w flashall |
一个人可以走的更快 , 一群人可以走的更远
]]>本文是Android性能优化工具系列的第一篇,这个系列主要介绍Android性能优化过程中会使用到的一些工具,以及如何用这些工具来发现问题和解决问题。在性能优化方面,Android有不少性能工具供大家来使用,按照我们一贯地 “发现问题-解决问题”的思路来看,发现问题才是最主要的,一上来就想着如何去解决问题,反而会事倍功半。
这一篇先来简单介绍一下Systrace这个工具。
Systrace是Android4.1中新增的性能数据采样和分析工具。它可帮助开发者收集Android关键子系统(如surfaceflinger、WindowManagerService等Framework部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。
Systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载以及Android各个子系统的运行状况等。在Android平台中,它主要由3部分组成:
使用Systrace前,要先了解一下Systrace在各个平台上的使用方法,鉴于大家使用Eclipse和Android Studio的居多,所以直接摘抄官网关于这个的使用方法,不过不管是什么工具,流程是一样的:
In Eclipse, open an Android application project.
In Android Studio, open an Android application project.
Navigate to your SDK tools/ directory.
命令行形式比较灵活,速度也比较快,一次性配置好之后,以后再使用的时候就会很快就出结果(强烈推荐)
1 | cd android-sdk/platform-tools/systrace |
从上面的命令可以看到Systrace工具的位置,只需要在Bash中配置好对应的路径和Alias,使用起来还是很快速的。另外User版本是不可以抓Trace的,只有ENG版本或者Userdebug版本才可以。
抓取结束后,会生成对应的Trace文件,注意这个文件只能被Chrome打开。关于如何分析Trace文件,我们下面的章节会讲。不论使用那种工具,在抓取之前都会让选择参数,下面说一下这些参数的意思:
-h, –help Show the help message.(帮助)
-o Write the HTML trace report to the specified file.(即输出文件名,)
-t N, –time=N Trace activity for N seconds. The default value is 5 seconds. (Trace抓取的时间,一般是 : -t 8)
-b N, –buf-size=N Use a trace buffer size of N kilobytes. This option lets you limit the total size of the data collected during a trace.
-k
—ktrace= Trace the activity of specific kernel functions, specified in a comma-separated list.
-l, –list-categories List the available tracing category tags. The available tags are(下面的参数不用翻译了估计大家也看得懂,贴官方的解释也会比较权威,后面分析的时候我们会看到这些参数的作业的):
-a
—app= Enable tracing for applications, specified as a comma-separated list of package names. The apps must contain tracing instrumentation calls from the Trace class. For more information, see Analyzing Display and Performance.
—link-assets Link to the original CSS or JavaScript resources instead of embedding them in the HTML trace report.
—from-file= Create the interactive Systrace report from a file, instead of running a live trace.
—asset-dir= Specify a directory for the trace report assets. This option is useful for maintaining a single set of assets for multiple Systrace reports.
-e
—serial= Conduct the trace on a specific connected device, identified by its device serial number.
上面的参数虽然比较多,但使用工具的时候不需考虑这么多,在对应的项目前打钩即可,命令行的时候才会去手动加参数:
我们一般会把这个命令配置成Alias,配置如下:
1 | alias st-start='python /home/gaojianwu/Software/android-studio/sdk/platform-tools/systrace/systrace.py' |
这样在使用的时候,可以直接敲 st-start 即可,当然为了区分和保持各个文件,还需要加上 -o xxx.Trace .上面的命令和参数不必一次就理解,只需要记住如何简单使用即可,在分析的过程中,这些东西都会慢慢熟悉的。
]]>一个人可以走的更快 , 一群人可以走的更远
作为Google的亲儿子,Nexus手机系列所收到的待遇大家有目共睹.Android5.0出来之后,Nexus5第一时间就升级到了最新的系统.那么作为Google亲儿子的Android Studio同样备受Google的重视,我也是第一时间从Eclipse转投到了Android Studio的怀抱中,从最初的测试版本一路升级到现在的1.0正式版本(今天发布了1.1,果断已经升级了).
关于Android Studio的好处我就不用说了,下面两点就足矣让你转投Android Studio了:
Android Lollipop是Google在今年推出的,关于Lollipop的详细介绍可以查看Lollipop官方介绍 ,我也就不多说了.作为一个开发者,我们不应该停留在表面(即Lollipop的绚丽的界面,和新奇的设计语言Material design,我们更要了解其中的原理.
在使用Android Studio查看源码之前,你需要做下面几件事:
mmm development/tools/idegen/
make completed successfully (43 seconds)
,如果编译失败了,后面会讲到这种情况(比较少见)sh ./development/tools/idegen/idegen.sh
执行第一个命令的时候编译不过,出现这种情况的原因有好几种:
执行第二个命令的时候,碰到下面的问题:
1 | Exception in thread "main" java.io.FileNotFoundException: ./out/target/product/hammerhead/obj/GYP/shared_intermediates/res.java (Is a directory) |
解决办法是将./out/target/product/hammerhead/obj/GYP/shared_intermediates/res.java
修改为:./out/target/product/hammerhead/obj/GYP/shared_intermediates/res.j
即可.
使用Android Studio看Android源码很爽,下面是他们的优缺点:
]]>一个人可以走的更快 , 一群人可以走的更远
上一篇文章从理论的角度讲解了一下什么是过渡绘制,以及可以用来查看和确认过渡绘制的工具,还提供了一些优化过渡绘制的方法。对代码和布局比较熟悉的人,看完上一篇其实就已经可以对自己的应用进行优化了。我记得有人说过,用iphone你只需要保证苹果有节操即可,用Android你就得保证所有的Android开发者都有节操。但现实是残酷的,现在Android市场上,有很多粗制滥造的应用,其中不乏大厂之作,各位打开过渡绘制按钮,就知道我所言非虚。作为一个Android开发人员,我肯定是更希望Android能一步一步好起来,超越iphone。
这篇文章从实战的角度,讲解了一个过渡绘制的优化过程。当然这里用到的只是很少的一部分,毕竟每个应用差别很大,优化方式也各不一样。所以这篇文章仅供参考,想把这块做好还是要下功夫的。
如果没有看过前一篇,可以点这里:Android性能优化之过渡绘制(一)
设置—辅助功能–开发人员工具–硬件加速渲染—调试GPU过渡绘制— 显示过渡绘制区域.
Kill掉(即清后台)要测试的应用,重新打开就可以看到效果.下面以文件管理器和设置为例子,如下图
从图上可以看出,按照过渡绘制从好到坏(蓝-绿-粉红-红)来看,文件管理器的过渡绘制是非常严重的,而设置界面的过渡绘制则在可以接受的范围内.下面就以文件管理器为主要分析对象,来看看如何对文件管理器的过渡绘制进行优化.
在进行位置确认后,我们大概确定了过渡绘制的区域,让我们来使用工具来进行验证和View确认.
打开Monitor(Eclipse和Android Studio中都有快捷打开按钮,即DDMS,右上角选择 Hierarchy View,大概使用如图
其中根节点:PhoneWindos$DecorView是整个视图的根节点,唯一的子节点是ActionBarOverlayLayout,这个Layout包含了ActionBar,应用程序,以及SmartBar.
下面讲述如何从Hierarchy View结合代码分析出需要进行修改的区域
上面分析过渡绘制区域的第一条,整个window存在一个背景,所以进行了一次重绘,这个背景的重绘是系统级别的,和主题有关,即这个背景是属于ActionBarOverlayLayout的.这种类型的过渡绘制解决也比较方便,在文件管理器的主Activity的onCreate方法中,加入
this.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
就可以将这个看不见的主题背景去掉.下面是去掉主题背景后的效果图(一张是划开,一张是没有划开):
对比优化前的图可以发现,背景被去掉之后,少了一层过渡绘制. ActionBar上的蓝色已经消失了.中间的内容由绿色变为蓝色
上面分析的第二条说”中间的内容部分,最底层是绿色,说明进行了2x的过渡绘制”,现在中间部分变成了蓝色,但是这是一个全局的背景,导致右边的view拉过来之后,还是存在大量的红色和绿色. 继续分析Hierarchy View,找到中间view对应的视图:DragRelativeLayout,查看源码可知,DragRelativeLayout继承自公共控件:SlidingMenu ,SlidingMenu 由CustomViewAbove和CustomViewBehind组成,前者是上面可以左右拉动的那部分,后者是底部不能拉动的那部分(这个从HierarchyView中也可以看出来:如下图所示:
点击CustomViewBehind,查看其所占的区域,就可以发现背景是这个View进行绘制的,打开CustomViewBehind的代码可以发现其构造函数中包含下面的代码:setBackgroundColor(getResources().getColor(R.color.mz_slidingmenu_background_light));
这个背景是不需要的,查看源码可知,这个view会在SlidingMenu.setMenu的时候,被覆盖掉,还是看不到的.所以这一层view是可以去掉的.下面是去掉一层背景之后的预览图:
可以看到这一层背景去掉之后,过渡绘制减轻了很多.
接着进行分析,可以看到CustomViewAbove也是存在一个过渡绘制的背景的,查看Hierarchy View的CustomViewAbove的子节点,可以看到过渡绘制是由ListView导致的.其id为:FilesList,在代码中找到它,并对他进行分析.在我将PartitionItemLayout中onDraw()函数的setBounds去掉之后,过渡绘制进一步改善了(但是ListItem的View的颜色也比之前要浅了,这一步优化需要根据具体情况进行) 下面是优化后的效果图:
可以看到图中的过渡绘制已经非常少了.!点个赞!
Lint工具的使用比较简单,根据给出的提示做对应的修改即可.有时候需要工具具体情况来确定是否需要修改. 下图是一个简单地例子.箭头处提示这个Layout或者它的父Layout是不必须的.具体修改方法即去掉FrameLayout,将RelativeLayout提升为根VIew即可.
Lint工具还会针对代码中潜在的不合理或者Bed Code做出修改意见.比较重要的提示包括
Tracer工具也在Android Device Monitor中.点击右上角的Tracer for OpenGL ES按钮就可以进入(如果没有这个按钮,点击旁边的Open Perspective按钮,从选项中选择Tracer for OpenGL ES即可).初次打开Tracer工具,里面是没有内容的,点击右上角的两个按钮(一个是打开现有的GLTrace文件,另一个是新建GLTrace文件)。点击Trace按钮, 手机会自动启动应用程序并启动对应的Activity,当手机上的内容完全绘制出来之后,就可以点击Stop按钮,生成GlTrace文件.文件会自动打开.
分析GLTrace文件,下图是优化过后的图,对比优化前的图可以发现,优化后不会去绘制默认的背景图和CustomViewBehind的背景图.
这只是一帧的绘制,如果多操作几下生成多个帧的绘制trace文件,会发现这两个背景会被多次的重绘,去掉后不仅会减轻过渡绘制,也会加快GUP的绘制速度.
作者:Gracker
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
打赏一下: 微博打赏
]]>一个人可以走的更快 , 一群人可以走的更远
首先将讲解一下GPU过渡绘制,也是开发者最直接接触的部分吧,这个内容将分为两个部分来将讲,第一部分初步讲解一下gpu过渡绘制的原理,和一些优化建议,第二部分将用实际例子来讲解优化GPU过渡绘制的一般步骤。
GPU过渡绘制的概念:GPU过度绘制指的是在屏幕一个像素上绘制多次(超过一次),比如一个TextView后有背景,那么显示文本的像素至少绘了两次,一次是背景,一次是文本。GPU过度绘制或多或少对性能有些影响,设备的内存带宽是有限的,当过度绘制导致应用需要更多的带宽(超过了可用带宽)的时候性能就会降低。带宽的限制每个设备都可能是不一样的。
Android提供了三个工具来帮助辨别和解决重绘问题:Hierachy Viewer,Tracer for OpenGL和Show GPU overdraw。前两个可以在ADT工具或者独立的monitor工具中找到,最后一个是在开发者选项的一部分.
Lint工具:
Lint工具的提升例子(摘自官方文档):
Hierarchy Viewer:此工具是一个ADT工具(或者monitor,最新版本的SDK建议不使用独立的HV工具,而是直接在monitor中进行操作.)的一部分,可以被用作对视图层级进行快速解读。在处理布局问题时特别有用,对于性能问题也很适用。Hierarchy Viewer默认只能在非加密设备使用,例如工程机,工程平板或者模拟器。为了能够在任何手机上使用Hierarchy Viewer,你得在你的应用中添加ViewServer,这是一个开源库,使用方法可以参考这里。连接上设备,打开Hierarchy Viewer(定位到tools/目录下,直接执行hierarchyviewer的命令,选定需要查看的Process,再点击Load View Hierarchy会显示出当前界面的布局Tree。在每个模块的Traffic light上有三个灯,分别代表了Measure, Layout and Draw三个步骤的性能。
在Android UI布局过程中,通过遵守一些惯用、有效的布局原则,我们可以制作出高效且复用性高的UI,概括来说包括如下几点:
尽量多使用RelativeLayout和LinearLayout, 不要使用绝对布局AbsoluteLayout,
将可复用的组件抽取出来并通过include标签使用;
使用ViewStub标签来加载一些不常用的布局;
动态地inflation view性能要比SetVisiblity性能要好.当然用VIewStub是最好的选择.
使用merge标签减少布局的嵌套层次
去掉多余的背景颜色(查看背景颜色是否多余,可以将HierarchyView中的图导出为psd文件,然后用Photoshop查看.具体可以参考这个视频)
内嵌使用包含layout_weight属性的LinearLayout会在绘制时花费昂贵的系统资源,因为每一个子组件都需要被测量两次。在使用ListView与GridView的时候,这个问题显的尤其重要,因为子组件会重复被创建.所以要尽量避免使用Layout_weight
使得Layout宽而浅,而不是窄而深(在Hierarchy Viewer的Tree视图里面体现)
另外有能力看源码的同学,下面是绘制OverDraw的源码位置:/frameworks/base/libs/hwui/OpenGLRenderer.cpp,有兴趣的可以去研究研究。
1 | void OpenGLRenderer::renderOverdraw() { |
还有QA可能用得到的一个指标:OverDraw数值,这个的源码位置在Framework/base/core/java/android/view/HardwareRender.java中(5.0中去掉了这个数值的显示)
1 | private void debugOverdraw(View.AttachInfo attachInfo, Rect dirty, |
]]>一个人可以走的更快 , 一群人可以走的更远
这里有个很简单也很实用的技巧,即在EditText的父Layout中,加入下面的两个属性即可:
android:focusable="true" android:focusableInTouchMode="true"
这样做的原理是让用户进入到这个页面之后,EditText的父控件 获取焦点,这样的话EditText就获取不到焦点,软键盘也不会自动弹起.只有在点击EditText的时候,软键盘才会弹起.
]]>一个人可以走的更快 , 一群人可以走的更远
DateUtils.formatDateTime() 用来进行区域格式化工作,输出格式化和本地化的时间或者日期.
[AlarmManager.setInexactRepeating](http://developer.android.com/reference/android/app/AlarmManager.html#setInexactRepeating(int, long, long, android.app.PendingIntent)) 通过闹铃分组的方式省电,即使你只调用了一个闹钟,这也是一个好的选择,(可以确保在使用完毕时自动调用 AlarmManager.cancel ()。原文说的比较抽象,这里详细说一下:setInexactRepeating指的是设置非准确闹钟,使用方法:alarmManager.setInexactRepeating(AlarmManager.RTC, startTime,intervalL, pendingIntent),非准确闹钟只能保证大致的时间间隔,但是不一定准确,可能出现设置间隔为30分钟,但是实际上一次间隔20分钟,另一次间隔40分钟。它的最大的好处是可以合并闹钟事件,比如间隔设置每30分钟一次,不唤醒休眠,在休眠8小时后已经积累了16个闹钟事件,而在手机被唤醒的时候,非准时闹钟可以把16个事件合并为一个, 所以这么看来,非准时闹钟一般来说比较节约能源.
[Formatter.formatFileSize()](http://developer.android.com/reference/android/text/format/Formatter.html#formatFileSize(android.content.Context, long)) 一个区域化的文件大小格式化工具。通俗来说就是把大小转换为MB,G,KB之类的字符串.
ActionBar.hide()/.show() 顾名思义,隐藏和显示ActionBar,可以优雅地在全屏和带Actionbar之间转换.
[Linkify.addLinks()](http://developer.android.com/reference/android/text/util/Linkify.html#addLinks(android.text.Spannable, int)) 在Text上添加链接.很实用.
StaticLayout 在自定义 View 中渲染文字的时候很实用。
Activity.onBackPressed() 很方便的管理back键的方法,有时候需要自己控制返回键的事件的时候,可以重写一下.比如加入 “点两下back键退出” 功能.
GestureDetector 用来监听和相应对应的手势事件,比如点击,长按,慢滑动,快滑动,用起来很简单,比你自己实现要方便许多.
DrawFilter 可以让你在不调用onDrew方法的情况下,操作canvas,比了个如,你可以在创建自定义 View 的时候设置一个 DrawFilter,给父 View 里面的所有 View 设置反别名。
ActivityManager.getMemoryClass() 告诉你你的机器还有多少内存,在计算缓存大小的时候会比较有用.
ViewStub 它是一个初始化不做任何事情的 View,但是之后可以载入一个布局文件。在慢加载 View 中很适合做占位符。唯一的缺点就是不支持标签,所以如果你不太小心的话,可能会在视图结构中加入不需要的嵌套。
SystemClock.sleep() 这个方法在保证一定时间的 sleep 时很方便,通常我用来进行 debug 和模拟网络延时。
DisplayMetrics.density 这个方法你可以获取设备像素密度,大部分时候最好让系统来自动进行缩放资源之类的操作,但是有时候控制的效果会更好一些.(尤其是在自定义View的时候).
[Pair.create()](http://developer.android.com/reference/android/util/Pair.html#create(A, B)) 方便构建类和构造器的方法。
原文地址:http://blog.danlew.net/2014/03/30/android-tips-round-up-part-2/
原文作者:http://blog.danlew.net/about/
本文地址:https://www.androidperformance.com/android-tips-round-up-2.html 转载请注明.
]]>一个人可以走的更快 , 一群人可以走的更远
Activity.startActivities() 常用于在应用程序中间启动其他的Activity.
TextUtils.isEmpty() 简单的工具类,用于检测是否为空
Html.fromHtml() 用于生成一个Html,参数可以是一个字符串.个人认为它不是很快,所以我不怎么经常去用.(我说不经常用它是为了重点突出这句话:请多手动构建 Spannable 来替换 Html.fromHtml),但是它对渲染从 web 上获取的文字还是很不错的。
TextView.setError() 在验证用户输入的时候很棒
Build.VERSION_CODES 这个标明了当前的版本号,在处理兼容性问题的时候经常会用到.点进去可以看到各个版本的不同特性
Log.getStackTraceString() 方便的日志类工具,方法Log.v()、Log.d()、Log.i()、Log.w()和Log.e()都是将信息打印到LogCat中,有时候需要将出错的信息插入到数据库或一个自定义的日志文件中,那么这种情况就需要将出错的信息以字符串的形式返回来,也就是使用static String getStackTraceString(Throwable tr)方法的时候.
LayoutInflater.from() 顾名思义,用于Inflate一个layout,参数是layout的id.这个经常写Adapter的人会用的比较多.
ViewConfiguration.getScaledTouchSlop() 使用 ViewConfiguration 中提供的值以保证所有触摸的交互都是统一的。这个方法获取的值表示:用户的手滑动这个距离后,才判定为正在进行滑动.当然这个值也可以自己来决定.但是为了一致性,还是使用标准的值较好.
PhoneNumberUtils.convertKeypadLettersToDigits 顾名思义.将字母转换为数字,类似于T9输入法,
Context.getCacheDir() 获取缓存数据文件夹的路径,很简单但是知道的人不多,这个路径通常在SD卡上(这里的SD卡指的是广义上的SD卡,包括外部存储和内部存储)Adnroid/data/您的应用程序包名/cache/ 下面.测试的时候,可以去这里面看是否缓存成功.缓存在这里的好处是:不用自己再去手动创建文件夹,不用担心用户把自己创建的文件夹删掉,在应用程序卸载的时候,这里会被清空,使用第三方的清理工具的时候,这里也会被清空.
ArgbEvaluator 用于处理颜色的渐变。就像 Chris Banes 说的一样,这个类会进行很多自动装箱的操作,所以最好还是去掉它的逻辑自己去实现它。这个没用过,不明其所以然,回头再补充.
ContextThemeWrapper 方便在运行的时候修改主题.
Space space是Android 4.0中新增的一个控件,它实际上可以用来分隔不同的控件,其中形成一个空白的区域.这是一个轻量级的视图组件,它可以跳过Draw,对于需要占位符的任何场景来说都是很棒的。
ValueAnimator.reverse() 这个方法可以很顺利地取消正在运行的动画.我超喜欢.
原文地址:http://blog.danlew.net/2014/03/30/android-tips-round-up-part-1/
原文作者:http://blog.danlew.net/about/
本文地址:https://www.androidperformance.com/android-tips-round-up-1.html 转载请注明.
]]>一个人可以走的更快 , 一群人可以走的更远
这篇文章最早是在我的CSDN博客上面发布了:http://blog.csdn.net/grackergao/article/details/18322749 .现在讲他转移到了这里,代码的Github地址 :https://github.com/Gracker/Android-Utils/blob/master/Log2File.java
import java.io.BufferedWriter;import java.io.File;import java.io.FileWriter;import java.io.IOException;import java.util.Date;import android.content.Context;import android.os.Environment;public class Log2File{ private static boolean logInit; private static BufferedWriter writer; private Log2File() { } /** * 初始化Log,创建log文件 * @param ctx * @param fileName * @return */ public static boolean init(Context ctx, String fileName) { if(!logInit) { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { File sdDir = Environment.getExternalStorageDirectory(); File logDir = new File(sdDir.getAbsolutePath() + "/log2file/" + ctx.getPackageName() + "/"); try { if(!logDir.exists()) { logDir.mkdirs(); } File logFile = new File(logDir, fileName); logFile.createNewFile(); writer = new BufferedWriter(new FileWriter(logFile, true)); logInit = true; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return logInit; } /** * 写一条log * @param msg */ public static void w(String msg) { if(logInit) { try { Date date = new Date(); writer.write("[" + date.toLocaleString() + "] " + msg); writer.newLine(); writer.flush(); } catch (IOException e) { // TODO Auto-generated catch block } } } /** * 关闭log */ public static void close() { if(logInit) { try { writer.close(); writer = null; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } logInit = false; } }}
Log2File.init(context, fileName);2.调用w()进行输出
Log2File.w(String msg);3.使用完毕后,记得要关闭Log
Log2File.close();
]]>一个人可以走的更快 , 一群人可以走的更远
上一篇文章介绍了什么是Accessibility以及简单的使用,这一篇文章就来讲讲如何使用Accessibility服务来创建一个简单的Android通知中心。Android中通知中心是一个系统层面的服务,负责显示应用和系统发来的通知(Notification,比如USB插入、选择输入法、未接来电、截图、天气信息、新闻推送等等)。在android4.3之前,一般的第三方应用是无法获取Notification list的(在Android4.3之后,有了一个新的接口,NotificationListenerService.getActiveNotifications(),可以获取当前的Notification)。但是利用Accessibility服务可以监听到各种事件的特性,可以开发一个第三方的通知中心,实现与系统通知栏类似的功能。
下面就来介绍如何开发自己的通知中心。
按照上一篇辅助性服务的介绍,一个辅助性服务可以被捆绑到一个标准的应用程序上,或者以一个单独的安卓工程被创建,我们这里建立一个服务,继承AccessibilitySerivce
1 | import android.accessibilityservice.AccessibilityService; |
继承AccessibilitySerivce必须要重写几个重要的方法:
1 | Notification localNotification = (Notification)event.getParcelableData(); |
得到Notification对象之后,就可以进行自己的操作,我这里是通过广播的形式,将收到的Notification发送给Activity进行处理。
这里也会碰到一个小问题:当一个Notification对象太大时(比如截图、未接来电等,Notification.contentView就很大,通过广播传播会出现data过大无法传输的问题),这时可以把Notification.contentView对象暂时保存在Application中,然后再置为null,Activity中接收到数据后,再进行赋值。
1 | <service |
这里就是普通的service注册,注意 <mate-data>标签中的xml文件:从Android 4.0版本开始,有另外一种方法:使用XML文件来配置这类服务。如果你以XML的形式来定义你的服务,某些像canRetrieveWindowContent可配置的选项就可用了。和service一样的配置,使用XML来定义。如果你要使用XML路径,要在你的mainfest文件中指定它,在你的服务声明中添加<meta-data>标签,并指向这个XML资源文件。比如上面的代码,我们在res/xml/中建立accessibilityaseviceconfig.xml,内容如下:
1 |
服务这里就配置好了。
下面的Activity中就可以接受这个数据,然后怎么处理就看自己了,这里只是简单地显示出来。
1 | import android.app.Activity; |
注:这里有很重要的一点,由于AccessibilityService的特殊性,用户必须手动到设置-辅助功能中,打开对应的服务,我们才可以通过AccessibilityService获得对应的数据,这一点非常重要。
上面的Activity只是简单地显示Notification,关于更多Notification的操作,可以参考Notification这个类,其中重要的属性有:contentView,flags。要模拟真正的通知中心,还是要费一番功夫的。这里由于公司项目的保密,暂不提供对应的实现代码(其实得到Notification就已经成功了一半了),有兴趣的同学可以私下和我交流。
AccessibilityService的实战就讲到这里,这一篇博文也是拖了一段时间才写完的,也算是为前一段时间的项目做个了结。
项目中目前还存在的问题:
]]>一个人可以走的更快 , 一群人可以走的更远
最近开发Nokia项目,遇到的问题如下:
插入Nokia x后,电脑没有反应,即不识别,同事的windows也不识别,最后在谷歌上搜索了良久,才找到了解决方案,但是没有记录,后来又要给别人配置的时候,发现忘记怎么配置了。想想这也是一个具有通性的问题,还是记录下来,分享给大家。
首先问题是:执行adb命令提示找不到设备,在做其他操作之前,请先确认已经做了如下操作:
~ » lsusb Bus 002 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching HubBus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hubBus 001 Device 002: ID 8087:0024 Intel Corp. Integrated Rate Matching HubBus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hubBus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hubBus 003 Device 004: ID 1532:0016 Razer USA, Ltd DeathAdder MouseBus 003 Device 003: ID 05d5:624c Super Gate Technology Co., Ltd Bus 003 Device 033: ID 0421:06e8 Nokia Mobile Phones Bus 003 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hu
vim /etc/udev/rules.d/51-android.rules 添加这个ID:1ebf,如下: SUBSYSTEM=="usb", SYSFS{"Nokia Mobile Phones"}=="0421", MODE="06e8" 保存文件并运行: sudo chmod a+rx /etc/udev/rules.d/51-android.rules sudo /etc/init.d/udev restart 运行结果如下: Rather than invoking init scripts through /etc/init.d, use the service(8) utility, e.g. service udev restart Since the script you are attempting to invoke has been converted to an Upstart job, you may also use the stop(8) and then start(8) utilities, e.g. stop udev ; start udev. The restart(8) utility is also available. udev stop/waiting udev start/running, process 14636
cd ~/tools/android-sdk-linux_x86/platform-tools sudo ./adb kill-server sudo ./adb start-server
注:一般情况下,上面的操作就可以。特殊情况下,usb设备还是不能被识别,比如我手上这台Nokia X。那么继续:
# ANDROID 4RD PARTY USB VENDOR ID LIST -- DO NOT EDIT.# USE 'android update adb' TO GENERATE.# 1 USB VENDOR ID PER LINE#for nokia x0x0421
保存关闭后,就可以识别了。windows下也是如此,不多叙述了。
]]>一个人可以走的更快 , 一群人可以走的更远
辅助性服务是安卓框架的一个特性,它的设计是为了让已经安装在安卓设备上的应用程序能够为用户提供一种导航式(引导式)回应。一个辅助性服务能够传达给
用户关于这个应用程序的利益,例如把文本转换成语音、当用户手指停留屏幕的一个重要区域时的haptic反馈。这一节涵盖了怎样去创建一个辅助性服务,如何处理应用程序的信息接收,还有如何把信息反馈给用户。
创建自己的辅助性服务
一个辅助性服务可以被捆绑到一个标准的应用程序上,或者以一个单独的安卓工程被创建。在任何情况下,创建这类服务的步骤都是一样的。在你的工程中,创建一个类继续AccessibilitySerivce。
1 | import android.accessibilityservice.AccessibilityService; |
像其他服务一样,你也可以在mainfest文件中声明它。记得要指定它处理android.accessibilityservice这个意图。以便当应用程序触发一个AccessibilityEvent时,这个服务能被调用。
1 | <application > |
如果你为这个服务创建一个新的工程的话,且不打算要一个应用程序,你可以把它启动活动的类(通常叫做MainActivity.java)从你的源文件中删除。同时也把相应的活动元素从你的mainfest文件中删除。
配置自己的辅助性服务
为你的辅助性服务设置配置变量,用它来告诉系统,如何和何时你想要它运行。哪一类事件你想要去响应?这个服务对所有的应用程序都是活动的吗?或者只有指定的包名的?它使用什么样的反馈?
你有两种方法去设置这些变量。反向兼容的方法是以代码的形式来设置它们。
可以使setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo).如果要这样做的话,要重写onServiceConnected()方法,然后配置在那里配置你的服务。
1 |
|
从Android 4.0版本开始,有另外一种方法:使用XML文件来配置这类服务。如果你以XML的形式来定义你的服务,某些像canRetrieveWindowContent可配置的选项就可用了。和上面一样的配置,使用XML来定义,格式如下所示:
1 | <accessibility-service |
如果你要使用XML路径,要在你的mainfest文件中指定它,在你的服务声明中添加 meta-data;标签,并指向这个XML资源文件。假如你把你的XML文件存储在res/xml/serviceconfig.xml这个路径下,新的标签格式如下所示:
1 | <service android:name=".MyAccessibilityService"> |
现在,您的服务被设置为运行和监听事件,写一些代码,这样当一个AccessibilityEvent真的到来,它就知道要做什么了!
从重写onAccessibilityEvent(AccessibilityEvent)方法开始。然后使用getEventType()来确定事件类型,然后用getContentDescription来取出任何与触发事件相关的标签文本。
1 |
|
为更多的上下文查询视图层次结构
这一步是可选的,然而它非常有用。Android 4.0(API level 14)的一个新特性:可以用AccessibilityService来查询视图层次结构,收集事件所生成的一些UI组件信息,还有这些UI的父控件和子控件。要做到这一点,确保在你的XML配置文件中做了如下设置:
1 | android:canRetrieveWindowContent="true" |
如果设置了,通过getSource()可获得一个AccessibilityNodeInfo对象。如果发起的窗口事件仍然是活动的窗口,该调用将会返回一个对象,否则,会返回null。下面这段代码演示何时接收一个事件,步骤如下:
立即捕获触发事件的父视图。
在那个视图中,寻找一个标签和一个复选框作的子视图。
如果找到,创建一个字符串来向用户报告,以表明这个标签是否被选择了。
如果遍历视图层次结构后返回null,则会退出该方法。
1 | // Alternative onAccessibilityEvent, that uses AccessibilityNodeInfo |
现在你有一个完整的,可以工作的辅助性服务。现在,你也可以试着配置一下,看看Android的text-to-speech engine,或使用Vibrator提供触觉反馈是如何与用户交互。
最后,要使用配置好的service,必须要到“设置–辅助性服务”中打开对应的service,然后才能相应对应的事件。
]]>一个人可以走的更快 , 一群人可以走的更远