Title: Hidden Gems in HarmonyOS Development! Practical Memory Optimization Guide for Smoother and More Power-Efficient Apps
Hey, HarmonyOS Developers!
Have you ever felt that while the official documentation is comprehensive, it’s like a huge treasure trove with many practical “gold mines” hidden inside, which you might miss if you don’t dig deep? Recently, I stumbled upon a treasure chapter about memory optimization. The tools and techniques provided are simply amazing! Many of the cases and methods can be lifesavers in real development, helping you avoid app lag, crashes, and even extend device battery life.
Today, I’ll share this treasure with you, combining official content and my own understanding, into this highly practical memory optimization guide. No fluff—just real tips, case studies, and code! 💪
🧠 Why Is Memory Optimization So Important? (Official Essence + Plain Talk)
The official docs say it well: Memory is a scarce system resource. If your app uses too much, the system gets overwhelmed (frequent recycling and allocation), and your app lags, slows down, or even crashes! Imagine your phone’s background frantically “cleaning up”—how can your app not lag?
- Benefits of Memory Optimization: Smoother apps, faster response, less system resource usage, longer device battery life (users love it!).
- In short: Good memory management = good app experience, happy users, happy boss, and peace of mind for you!
🔍 Gem 1: Insight into Memory Usage - Official Toolchain (HiDumper
)
The official docs mention a powerful command-line tool, HiDumper
, which lets you view detailed memory info of your app directly on the device. This is much more intuitive than looking at abstract data in the IDE!
📌 Practical Steps & Explanation
- Find Your App’s Process ID (PID):
hdc shell "hidumper -s WindowManagerService -a '-a'"
- This command lists all window-related info. Find your app’s package name (e.g.,
com.example.myawesomeapp
) and its PID. This PID is your app’s system ID.
- View Detailed Memory Report:
hdc shell "hidumper --mem [YourAppPID]"
- Replace
[YourAppPID]
with the actual number found above. You’ll get a detailed memory report.
-
Key Metrics - Focus!
There’s a lot of data, but the official docs suggest focusing on the PSS (Proportional Set Size) in the Total column.
- What is PSS? It’s the actual physical memory used by your app. It’s the gold standard for measuring memory usage, more accurate than just virtual memory (VSS) or resident memory (RSS), because it accounts for shared libraries.
- Sample Report Key Part (Simplified):
... (other info) ...
PSS: 26279 kB (TOTAL)
... (memory breakdown) ...
ark ts heap: 4712 kB
native heap: 13164 kB
... (other stacks, shared libs, etc.) ...
- **Interpretation:** This app uses about 26.3MB of physical memory in total.
* `ark ts heap (4712KB ≈ 4.6MB)`: This is the heap memory used by your ArkTS code (mainly JS/TS objects). This is the **main battlefield** for optimization!
* `native heap (13164KB ≈ 12.8MB)`: This is memory allocated by the Native layer (C/C++ code, third-party native libs, some system frameworks). If this is abnormally high, check for memory leaks or large object allocations in your native code or libraries.
💡 Developer Perspective
- When to use? If your app feels laggy or you suspect high memory usage, use this command first to check the overall situation and the proportion of each part. Quickly locate whether it’s a JS layer or Native layer issue.
- Comparative Analysis: Run the command multiple times in different scenarios (just launched, after feature operations, after running in the background for a while) and compare PSS changes to find memory growth points.
-
What if Native Heap is high? Use memory snapshot analysis (
DevEco Profiler
) or native memory analysis tools (likeasan
, also supported by HarmonyOS) for deeper investigation.
🕵️♂️ Gem 2: Catching Memory Leaks - The Magic Tool DevEco Profiler
The official docs mention two core memory analysis features in DevEco Studio’s built-in Profiler:
-
Allocation Tracking:
- What does it do? Monitors in real time when and where (which thread, which call stack) your app allocates different types of memory objects.
-
Practical Uses:
- Find places where many small objects are allocated in a short time (may cause frequent GC and lag).
- Locate the source of large object allocations.
- Observe if memory allocation hotspots are reasonable during different operations.
-
Heap Snapshot:
- What does it do? Takes a “photo” of your app’s memory heap at a specific moment (e.g., when you suspect a leak), recording all live objects and their references.
-
Golden Method for Catching Leaks:
- Before the suspected leak scenario (e.g., opening a page), manually trigger a GC (garbage collection).
- Take a snapshot
Snapshot 1
. - Perform the suspected leak operation (e.g., repeatedly open/close the page).
- After the operation, manually trigger GC again.
- Take a second snapshot
Snapshot 2
. - Profiler provides a comparison feature (
Compare to previous snapshot
orCompare to baseline snapshot
). -
Focus on: Objects that increase in the comparison (
Delta
** /+
sign)!** Especially those that should have been collected after the operation (and GC). If a class/object’s count or total size keeps growing, and you can’t find a reasonable holder (e.g., held by a global variable or long-lived object), it’s likely a leak!
💡 Developer Perspective & Tips
-
Allocation in Practice: Suspect a list scroll is laggy? Turn on Allocation Tracking, scroll the list, and see if
build()
or data updates are creating lots of small objects (like temp strings, small arrays). Optimization may involve reusing objects, avoiding heavy computation or large temp objects inbuild()
. - Snapshot Comparison in Practice (Suspected Page Leak):
// Suppose we have a potentially leaking page PageA
import { BusinessError } from '@ohos.base';
@Entry
@Component
struct PageA {
private expensiveData: any[] = []; // Suppose this holds a lot of data
// Simulate loading data (may not be released properly)
loadData() {
// ... fetch data, assign to expensiveData ...
}
onPageShow() {
this.loadData();
}
// ⚠️ Problem: Missing onPageHide or aboutToDisappear to release expensiveData!
// When the page is popped from the stack, expensiveData may not be released due to references
}
- **Operation:** Repeatedly open and return from `PageA`.
- **Snapshot Comparison:** You’ll find that after each open/return, the number/size of `expensiveData` or its internal objects keeps increasing in the snapshot, and even manual GC can’t reclaim them. This strongly suggests that the `PageA` instance or `expensiveData` isn’t being released properly.
- **Fix:** Clear `expensiveData` in the `aboutToDisappear` lifecycle:
aboutToDisappear() {
this.expensiveData = []; // Release reference so GC can reclaim the data
}
-
Tip: Give key custom classes a clear, recognizable
constructor.name
to make them easier to spot in snapshots. In snapshots,Retained Size
is more important thanShallow Size
—it shows the total memory held by the object and all its dependencies.
🚨 Gem 3: Memory Warning and Self-Rescue - onMemoryLevel
Listener
This feature is critical! HarmonyOS notifies your app via the onMemoryLevel
callback when memory is tight. This is your app’s last chance to “save itself”! The official docs provide three registration methods:
-
AbilityStage: Suitable for HAP-level memory response. Handle in the
onMemoryLevel
method ofAbilityStage
. -
UIAbility (or base class Ability): Suitable for Ability-level memory response. Handle in the corresponding Ability’s
onMemoryLevel
method. -
EnvironmentCallback: Register via
ApplicationContext
, suitable for global memory monitoring associated with the app context.
📊 Memory Levels
The official docs define three levels, explained in plain English:
Level Constant | Value | System State | What Should Your App Do? |
---|---|---|---|
MEMORY_LEVEL_MODERATE |
0 | Memory starting to get tight. System kills background processes by LRU. | Be alert! Consider releasing non-core, rebuildable resources (e.g., image caches not in current view, some historical data). Assess if your app might be killed. |
MEMORY_LEVEL_LOW |
1 | Memory very low. System under heavy pressure, high risk of lag. | Must act! Immediately release as many non-essential resources as possible (clear most caches, pause non-critical background tasks, release temp large objects). |
MEMORY_LEVEL_CRITICAL |
2 | Memory critically low. System near collapse, even core processes may be killed. | Desperate self-rescue! Release everything that can be released (clear all caches, stop all background tasks, save only the absolute minimum state). Be ready to be killed at any time. |
💻 Code Example: Listen and Respond in UIAbility
import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';
import { logger } from './Logger'; // Assume a logging tool
import { myCacheManager } from './MyCacheManager'; // Assume a cache manager module
export default class EntryAbility extends UIAbility {
// ... other lifecycle methods ...
onMemoryLevel(level: AbilityConstant.MemoryLevel) {
logger.info(`[MemoryLevel] Received memory level: ${level}`);
switch (level) {
case AbilityConstant.MemoryLevel.MEMORY_LEVEL_MODERATE:
logger.warn('[MemoryLevel] MODERATE: Memory pressure is building. Releasing non-critical caches.');
// Release level-one non-core caches (e.g., images not used for a long time, data from non-current modules)
myCacheManager.releaseLevelOneCache();
break;
case AbilityConstant.MemoryLevel.MEMORY_LEVEL_LOW:
logger.error('[MemoryLevel] LOW: Memory is very low! Releasing most caches and pausing heavy tasks.');
// Release most caches, pause non-critical background tasks, network prefetch, etc.
myCacheManager.releaseMostCaches();
myBackgroundTaskManager.pauseNonCriticalTasks(); // Assume a background task manager
break;
case AbilityConstant.MemoryLevel.MEMORY_LEVEL_CRITICAL:
logger.fatal('[MemoryLevel] CRITICAL: Memory critical! Releasing EVERYTHING non-essential and saving state!');
// Clear all caches, stop all background tasks, save the most critical app state (e.g., user’s unsaved form draft)
myCacheManager.clearAllCaches();
myBackgroundTaskManager.stopAllTasks();
this.saveCriticalState(); // Custom method to save critical state
break;
default:
logger.warn(`[MemoryLevel] Unknown level received: ${level}`);
}
}
private saveCriticalState() {
// Save absolutely critical state here, e.g., user’s unsaved form input.
// Note: Be quick! Memory is almost gone!
// For example: persist to Preferences or Database.
}
}
💡 Developer Perspective & Important Tips
-
Choose the right registration point: If the response is global (like clearing cache), put it in
AbilityStage
orEnvironmentCallback
. If it’s closely related to a specific Ability’s state (like saving a page draft), put it in thatUIAbility
. -
Act fast! Especially at
LOW
andCRITICAL
levels, the system is in crisis—your response code must be efficient and quick. Avoid time-consuming operations (complex calculations, large file IO) in the callback. -
What to release? Plan your resource hierarchy in advance:
- Level-one cache (least important, can be rebuilt anytime)
- Level-two cache (a bit more important)
- Critical state (user data, current progress)
- Core memory needed for operation.
-
No callback when frozen in background: Note! The official docs clearly state: If your app is already frozen in the background, you won’t receive the
onMemoryLevel
callback! The system will manage frozen apps’ memory directly. So this mechanism is mainly for apps in the foreground or active in the background.
🎯 Summary & Action Recommendations
This set of “memory optimization gems” from the official docs is truly practical:
-
HiDumper - Macro Insight: Quickly locate memory hogs (PSS), distinguish between JS layer (
ark ts heap
) and Native layer (native heap
) issues. -
DevEco Profiler - Micro Analysis & Leak Detection:
-
Allocation Tracking
: Catch sources of short-term mass allocation/large object allocation. -
Heap Snapshot (Comparison)
: The gold standard for catching memory leaks—look atDelta
objects andRetained Size
.
-
- onMemoryLevel - Proactive Defense: Respond to system memory warnings, release resources in stages to survive, improve user experience and system stability.
Action Recommendations:
-
Make it a habit: Regularly (especially after key features are done) use
HiDumper
to check memory baselines during development. - Use Profiler wisely: Incorporate Allocation and Snapshot analysis into your testing process, especially for complex page navigation, large data loading, and long lists.
-
Implement
onMemoryLevel
: Don’t be lazy! Implement memory level response strategies for your app—it’s key to robustness and user reputation. - Follow official updates: HarmonyOS tools and APIs are evolving fast. Keep an eye on the docs and community for the latest optimization techniques and cases.
Hope this blend of official essence and practical experience helps you truly leverage these “gem” features of HarmonyOS, building apps with low memory usage, smooth performance, and high user satisfaction! If you discover other great tips or pitfalls in practice, feel free to share and discuss in the comments! Let’s improve together and make the HarmonyOS app ecosystem even better!
Happy Coding & Optimizing! 😄
Top comments (0)