Memory-Efficient UI Development
Introduction to Memory Management in Mobile UI
Understanding Memory Constraints
Mobile devices have limited memory resources compared to desktop systems. Efficient memory management is crucial for maintaining smooth performance and preventing application crashes due to out-of-memory errors.
Memory Management Fundamentals
- Heap Memory: Object allocation and deallocation
- Stack Memory: Local variables and method calls
- Memory Leaks: Objects that cannot be garbage collected
- Memory Pressure: System state when available memory is low
Memory Leak Prevention
Common Memory Leak Patterns
Android Memory Leak Prevention
kotlin
// Avoid static references to Activities
class MemoryLeakPreventor {
// BAD: Static reference to Activity
// companion object {
// var activity: Activity? = null
// }
// GOOD: Use WeakReference for Activity references
companion object {
private var activityRef: WeakReference<Activity>? = null
fun setActivity(activity: Activity) {
activityRef = WeakReference(activity)
}
fun getActivity(): Activity? = activityRef?.get()
}
}
// Proper listener cleanup
class ViewWithListener : View {
private var listener: (() -> Unit)? = null
fun setListener(listener: () -> Unit) {
this.listener = listener
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
listener = null // Prevent memory leak
}
}
iOS Memory Management
swift
// Use weak references to prevent retain cycles
class ViewController: UIViewController {
weak var delegate: ViewControllerDelegate?
private var timer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
setupTimer()
}
private func setupTimer() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateUI()
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
timer?.invalidate()
timer = nil
}
private func updateUI() {
// Update UI safely
}
}
// Protocol with weak delegate pattern
protocol ViewControllerDelegate: AnyObject {
func didUpdateData()
}
Automatic Memory Management
Flutter Memory Management
dart
// Proper disposal of controllers and streams
class MemoryEfficientWidget extends StatefulWidget {
@override
_MemoryEfficientWidgetState createState() => _MemoryEfficientWidgetState();
}
class _MemoryEfficientWidgetState extends State<MemoryEfficientWidget> {
late StreamController<String> _controller;
late ScrollController _scrollController;
StreamSubscription<String>? _subscription;
@override
void initState() {
super.initState();
_controller = StreamController<String>();
_scrollController = ScrollController();
_subscription = _controller.stream.listen((data) {
// Handle data
});
}
@override
void dispose() {
// Critical: Dispose all resources
_subscription?.cancel();
_controller.close();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
);
}
}
Object Pooling and Recycling
View Recycling Patterns
Android RecyclerView Optimization
kotlin
class EfficientAdapter(private val items: List<Item>) :
RecyclerView.Adapter<EfficientAdapter.ViewHolder>() {
// Object pool for expensive operations
private val textPaintPool = Pools.SimplePool<Paint>(10)
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val titleView: TextView = itemView.findViewById(R.id.title)
val imageView: ImageView = itemView.findViewById(R.id.image)
fun bind(item: Item) {
titleView.text = item.title
// Use efficient image loading
Glide.with(itemView.context)
.load(item.imageUrl)
.placeholder(R.drawable.placeholder)
.into(imageView)
}
fun recycle() {
// Clear resources when view is recycled
imageView.setImageDrawable(null)
titleView.text = null
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_layout, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
override fun onViewRecycled(holder: ViewHolder) {
super.onViewRecycled(holder)
holder.recycle()
}
override fun getItemCount(): Int = items.size
}
iOS Cell Reuse Optimization
swift
class EfficientTableViewCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var subtitleLabel: UILabel!
@IBOutlet weak var thumbnailImageView: UIImageView!
override func prepareForReuse() {
super.prepareForReuse()
// Reset cell state
titleLabel.text = nil
subtitleLabel.text = nil
thumbnailImageView.image = nil
// Cancel any ongoing image downloads
thumbnailImageView.sd_cancelCurrentImageLoad()
}
func configure(with item: Item) {
titleLabel.text = item.title
subtitleLabel.text = item.subtitle
// Use SDWebImage for efficient image caching
thumbnailImageView.sd_setImage(
with: URL(string: item.imageURL),
placeholderImage: UIImage(named: "placeholder")
)
}
}
Custom Object Pools
Generic Object Pool Implementation
kotlin
class ObjectPool<T>(
private val factory: () -> T,
private val reset: (T) -> Unit,
maxSize: Int = 10
) {
private val pool = LinkedList<T>()
private val maxPoolSize = maxSize
fun acquire(): T {
return if (pool.isNotEmpty()) {
pool.removeFirst()
} else {
factory()
}
}
fun release(obj: T) {
if (pool.size < maxPoolSize) {
reset(obj)
pool.addLast(obj)
}
}
}
// Usage example
class ExpensiveObject {
var data: String = ""
fun reset() {
data = ""
}
}
class PoolManager {
private val expensiveObjectPool = ObjectPool(
factory = { ExpensiveObject() },
reset = { it.reset() }
)
fun useExpensiveObject(): String {
val obj = expensiveObjectPool.acquire()
try {
obj.data = "Process some data"
return obj.data
} finally {
expensiveObjectPool.release(obj)
}
}
}
Virtual Lists and Lazy Loading
Virtual Scrolling Implementation
React Native Virtual List
javascript
import React, { useMemo, useCallback } from 'react';
import { VirtualizedList, View, Text } from 'react-native';
const VirtualList = ({ data }) => {
const getItem = useCallback((data, index) => data[index], []);
const getItemCount = useCallback((data) => data.length, []);
const renderItem = useCallback(({ item, index }) => (
<View style={{ height: 50, justifyContent: 'center', padding: 10 }}>
<Text>{item.title}</Text>
</View>
), []);
const keyExtractor = useCallback((item, index) => item.id || index.toString(), []);
return (
<VirtualizedList
data={data}
initialNumToRender={10}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemCount={getItemCount}
getItem={getItem}
maxToRenderPerBatch={5}
windowSize={10}
removeClippedSubviews={true}
/>
);
};
Flutter Lazy Loading
dart
class LazyListView extends StatefulWidget {
final List<Item> items;
const LazyListView({Key? key, required this.items}) : super(key: key);
@override
_LazyListViewState createState() => _LazyListViewState();
}
class _LazyListViewState extends State<LazyListView> {
final ScrollController _scrollController = ScrollController();
final Set<int> _loadedItems = {};
@override
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void _onScroll() {
final double scrollOffset = _scrollController.offset;
final double maxScrollExtent = _scrollController.position.maxScrollExtent;
// Preload items near the viewport
if (scrollOffset > maxScrollExtent * 0.8) {
_preloadItems();
}
}
void _preloadItems() {
// Implement preloading logic
}
@override
Widget build(BuildContext context) {
return ListView.builder(
controller: _scrollController,
itemCount: widget.items.length,
itemBuilder: (context, index) {
return LazyLoadItem(
item: widget.items[index],
isLoaded: _loadedItems.contains(index),
onLoad: () => _loadedItems.add(index),
);
},
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
class LazyLoadItem extends StatelessWidget {
final Item item;
final bool isLoaded;
final VoidCallback onLoad;
const LazyLoadItem({
Key? key,
required this.item,
required this.isLoaded,
required this.onLoad,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (!isLoaded) {
WidgetsBinding.instance.addPostFrameCallback((_) => onLoad());
return Container(
height: 60,
child: Center(child: CircularProgressIndicator()),
);
}
return ListTile(
title: Text(item.title),
subtitle: Text(item.subtitle),
);
}
}
Memory Profiling and Optimization
Memory Monitoring Tools
Android Memory Profiling
kotlin
class MemoryProfiler {
fun logMemoryUsage() {
val runtime = Runtime.getRuntime()
val usedMemory = runtime.totalMemory() - runtime.freeMemory()
val maxMemory = runtime.maxMemory()
val availableMemory = maxMemory - usedMemory
Log.d("Memory", "Used: ${usedMemory / 1024 / 1024}MB")
Log.d("Memory", "Max: ${maxMemory / 1024 / 1024}MB")
Log.d("Memory", "Available: ${availableMemory / 1024 / 1024}MB")
}
fun forceGarbageCollection() {
System.gc()
logMemoryUsage()
}
}
// Memory leak detection with LeakCanary integration
class Application : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
if (LeakCanary.isInAnalyzerProcess(this)) {
return
}
LeakCanary.install(this)
}
}
}
iOS Memory Tracking
swift
import os.log
class MemoryTracker {
private let logger = OSLog(subsystem: "com.app.memory", category: "tracking")
func logMemoryUsage() {
let info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size)/4
let kerr: kern_return_t = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_,
task_flavor_t(MACH_TASK_BASIC_INFO),
$0,
&count)
}
}
if kerr == KERN_SUCCESS {
let memoryUsage = info.resident_size / 1024 / 1024
os_log("Memory usage: %{public}d MB", log: logger, type: .info, memoryUsage)
}
}
}
Best Practices for Memory Efficiency
Image Memory Management
kotlin
// Android - Efficient image handling
class ImageManager {
companion object {
fun loadImageEfficiently(
imageView: ImageView,
imagePath: String,
targetWidth: Int,
targetHeight: Int
) {
// Calculate inSampleSize for memory efficiency
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(imagePath, options)
options.inSampleSize = calculateInSampleSize(options, targetWidth, targetHeight)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(imagePath, options)
imageView.setImageBitmap(bitmap)
}
private fun calculateInSampleSize(
options: BitmapFactory.Options,
reqWidth: Int,
reqHeight: Int
): Int {
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while ((halfHeight / inSampleSize) >= reqHeight &&
(halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
}
}
This comprehensive guide provides practical strategies for memory-efficient UI development across all major mobile platforms, focusing on leak prevention, object pooling, virtual lists, and performance monitoring.