Производительность — критически важный фактор успеха мобильных приложений. Пользователи ожидают быстрых, отзывчивых приложений, которые не разряжают батарею и не потребляют чрезмерные объемы данных. Плохая производительность приводит к негативным отзывам, повышенному оттоку пользователей и более низким рейтингам в app store. Это комплексное руководство охватывает основные техники профилирования производительности мобильных приложений, от обнаружения утечек памяти до оптимизации времени запуска, помогая вам создавать исключительные мобильные продукты.
Ландшафт мобильной производительности
Почему важна производительность
Производительность влияет на каждый аспект успеха мобильного приложения:
Метрика | Влияние |
---|---|
Время загрузки | 53% пользователей покидают приложения, которые загружаются >3 секунд |
Разряд батареи | Приложения, использующие >20% батареи ежедневно, удаляются |
Использование памяти | Приложения с высоким потреблением памяти вылетают в 3 раза чаще |
Размер приложения | Каждое увеличение на 6 МБ = падение конверсии на 1% |
Использование сети | Чрезмерное потребление данных = жалобы пользователей и удаления |
Комплексное тестирование производительности должно охватывать каждый из этих аспектов для обеспечения исключительного пользовательского опыта.
Обнаружение утечек памяти
Профилирование памяти в iOS с Instruments
Использование Memory Graph Debugger:
// Пример: Распространенный retain cycle с замыканиями
class UserViewController: UIViewController {
var userService: UserService?
override func viewDidLoad() {
super.viewDidLoad()
// ❌ ПЛОХО: Создает retain cycle
userService?.fetchUser { user in
self.updateUI(with: user) // 'self' захвачен сильно
}
// ✅ ХОРОШО: Использовать weak self
userService?.fetchUser { [weak self] user in
self?.updateUI(with: user)
}
// ✅ АЛЬТЕРНАТИВА: Использовать unowned, если self гарантированно существует
userService?.fetchUser { [unowned self] user in
self.updateUI(with: user)
}
}
private func updateUI(with user: User) {
// Обновить UI
}
}
Обнаружение утечек с помощью Unit Tests:
import XCTest
class MemoryLeakTests: XCTestCase {
func testViewControllerDoesNotLeak() {
let viewController = UserViewController()
// Отслеживать view controller слабо
weak var weakVC = viewController
// Представить и отклонить
let window = UIWindow()
window.rootViewController = viewController
window.makeKeyAndVisible()
// Имитировать отклонение
window.rootViewController = nil
// Проверить освобождение
XCTAssertNil(weakVC, "UserViewController должен быть освобожден")
}
func testImageCacheReleasesMemoryUnderPressure() {
let cache = ImageCache.shared
// Заполнить кэш изображениями
for i in 0..<100 {
let image = generateTestImage()
cache.store(image, forKey: "image-\(i)")
}
let initialMemory = cache.currentMemoryUsage
// Имитировать предупреждение о памяти
NotificationCenter.default.post(
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
// Дождаться очистки
let expectation = XCTestExpectation(description: "Memory cleanup")
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
expectation.fulfill()
}
wait(for: [expectation], timeout: 2)
let finalMemory = cache.currentMemoryUsage
// Память должна значительно уменьшиться
XCTAssertLessThan(
finalMemory,
initialMemory * 0.5,
"Кэш должен освободить минимум 50% памяти"
)
}
}
Профилирование памяти в Android
Использование LeakCanary:
// build.gradle
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
}
// Класс Application
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// LeakCanary инициализируется автоматически
// Настроить при необходимости
LeakCanary.config = LeakCanary.config.copy(
retainedVisibleThreshold = 3,
dumpHeap = true
)
}
}
Пользовательское обнаружение утечек памяти:
@RunWith(AndroidJUnit4::class)
class MemoryLeakTests {
@Test
fun activityDoesNotLeak() {
val scenario = ActivityScenario.launch(MainActivity::class.java)
// Сохранить слабую ссылку
var weakActivity: WeakReference<MainActivity>? = null
scenario.onActivity { activity ->
weakActivity = WeakReference(activity)
}
// Закрыть activity
scenario.close()
// Принудительная сборка мусора
Runtime.getRuntime().gc()
Thread.sleep(1000)
Runtime.getRuntime().gc()
// Проверить, что activity была собрана
assertNull(
"MainActivity должна быть собрана сборщиком мусора",
weakActivity?.get()
)
}
@Test
fun bitmapRecycling() {
val options = BitmapFactory.Options().apply {
inMutable = true
}
var bitmap: Bitmap? = BitmapFactory.decodeResource(
InstrumentationRegistry.getInstrumentation().context.resources,
R.drawable.test_image,
options
)
assertNotNull(bitmap)
val byteCount = bitmap!!.byteCount
// Утилизировать bitmap
bitmap.recycle()
bitmap = null
// Принудительная GC
Runtime.getRuntime().gc()
Thread.sleep(500)
// Проверить, что память была освобождена
val runtime = Runtime.getRuntime()
val freeMemory = runtime.freeMemory()
assertTrue(
"Память bitmap должна быть освобождена",
freeMemory > byteCount
)
}
}
Тестирование потребления батареи
Чрезмерный расход батареи — одна из главных причин удаления приложений. В рамках тестирования производительности мобильных приложений мониторинг батареи критически важен для удовлетворенности пользователей.
Профилирование батареи в iOS
Использование XCTest для влияния на энергопотребление:
class BatteryConsumptionTests: XCTestCase {
func testBackgroundLocationDoesNotDrainBattery() {
let app = XCUIApplication()
app.launch()
let metrics = [
XCTCPUMetric(),
XCTMemoryMetric(),
XCTStorageMetric()
]
let options = XCTMeasureOptions()
options.iterationCount = 3
measure(metrics: metrics, options: options) {
// Включить отслеживание местоположения
app.switches["Location Tracking"].tap()
// Имитировать фоновый режим
XCUIDevice.shared.press(.home)
// Ждать фоновой активности
Thread.sleep(forTimeInterval: 60)
// Вернуться в приложение
app.activate()
// Отключить местоположение
app.switches["Location Tracking"].tap()
}
}
}
Тестирование батареи в Android
Использование Battery Historian:
# Собрать данные о батарее
adb shell dumpsys batterystats --reset
adb shell dumpsys batterystats --enable full-wake-history
# Запустить тесты
./gradlew connectedAndroidTest
# Получить статистику батареи
adb bugreport bugreport.zip
# Анализировать с Battery Historian
# Загрузить bugreport.zip на https://bathist.ef.lc/
Автоматизированное тестирование батареи:
@RunWith(AndroidJUnit4::class)
class BatteryConsumptionTests {
private lateinit var powerManager: PowerManager
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
}
@Test
fun testWakeLockIsReleasedProperly() {
val wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"MyApp::TestWakeLock"
)
// Получить wake lock
wakeLock.acquire(10*60*1000L) // 10 минут
assertTrue(wakeLock.isHeld)
// Выполнить операцию
performNetworkSync()
// Освободить wake lock
if (wakeLock.isHeld) {
wakeLock.release()
}
assertFalse(wakeLock.isHeld)
}
@Test
fun testLocationUpdatesUseAppropriateInterval() {
val locationManager = ApplicationProvider
.getApplicationContext<Context>()
.getSystemService(Context.LOCATION_SERVICE) as LocationManager
val locationListener = object : LocationListener {
var updateCount = 0
var timestamps = mutableListOf<Long>()
override fun onLocationChanged(location: Location) {
updateCount++
timestamps.add(System.currentTimeMillis())
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
override fun onProviderEnabled(provider: String) {}
override fun onProviderDisabled(provider: String) {}
}
// Запросить обновления местоположения
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
60000L, // 1 минута минимум
100f, // 100 метров минимальное расстояние
locationListener
)
// Ждать и проверить частоту обновлений
Thread.sleep(180000) // 3 минуты
locationManager.removeUpdates(locationListener)
// Должно быть примерно 3 обновления (одно в минуту)
assertTrue(
"Обновления местоположения слишком частые: ${locationListener.updateCount}",
locationListener.updateCount <= 4
)
// Проверить интервалы между обновлениями
for (i in 1 until locationListener.timestamps.size) {
val interval = locationListener.timestamps[i] - locationListener.timestamps[i-1]
assertTrue(
"Интервал обновления слишком короткий: ${interval}ms",
interval >= 55000 // Допустить 5с погрешности
)
}
}
}
Оптимизация сети
Мониторинг эффективности сети
Тестирование сети в iOS:
class NetworkPerformanceTests: XCTestCase {
func testAppHandles3GNetworkWell() {
let app = XCUIApplication()
app.launchArguments = ["NETWORK_3G"]
app.launch()
measure(metrics: [XCTClockMetric()]) {
app.buttons["Refresh"].tap()
let loadingComplete = app.staticTexts["Content Loaded"]
let exists = loadingComplete.waitForExistence(timeout: 10)
XCTAssertTrue(exists, "Контент должен загрузиться за 10с на 3G")
}
}
func testImageLoadingOptimization() {
let app = XCUIApplication()
app.launch()
// Перейти к галерее изображений
app.tabBars.buttons["Gallery"].tap()
measure(metrics: [XCTClockMetric(), XCTMemoryMetric()]) {
// Прокрутить изображения
let collectionView = app.collectionViews.firstMatch
for _ in 0..<20 {
collectionView.swipeUp()
}
}
}
}
Профилирование сети в Android:
@RunWith(AndroidJUnit4::class)
class NetworkOptimizationTests {
@Test
fun testRequestBatching() {
val mockServer = MockWebServer()
mockServer.start()
var requestCount = 0
mockServer.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
requestCount++
return MockResponse().setResponseCode(200).setBody("{}")
}
}
// Инициировать несколько операций, требующих данных
val scenario = ActivityScenario.launch(MainActivity::class.java)
scenario.onActivity { activity ->
activity.loadUserData()
activity.loadPreferences()
activity.loadNotifications()
}
Thread.sleep(2000) // Ждать окна группировки
// Должны сгруппироваться в один запрос или минимум запросов
assertTrue(
"Слишком много сетевых запросов: $requestCount",
requestCount <= 2 // Допустить некоторую гибкость
)
mockServer.shutdown()
}
@Test
fun testResponseCaching() {
val context = ApplicationProvider.getApplicationContext<Context>()
val cacheDir = context.cacheDir
val cacheSize = 10 * 1024 * 1024 // 10 MB
val cache = Cache(cacheDir, cacheSize.toLong())
val client = OkHttpClient.Builder()
.cache(cache)
.build()
val request = Request.Builder()
.url("https://api.example.com/users")
.build()
// Первый запрос - должен обратиться к сети
val response1 = client.newCall(request).execute()
assertNotNull(response1.networkResponse)
assertNull(response1.cacheResponse)
response1.close()
// Второй запрос - должен использовать кэш
val response2 = client.newCall(request).execute()
assertNull(response2.networkResponse)
assertNotNull(response2.cacheResponse)
response2.close()
}
}
Оптимизация размера приложения
Измерение размера приложения
iOS App Thinning:
# Генерировать отчет о размере приложения
xcodebuild -exportArchive \
-archivePath MyApp.xcarchive \
-exportPath ./Export \
-exportOptionsPlist ExportOptions.plist
# Анализировать размер приложения по устройствам
ls -lh Export/*.ipa
Android APK Analyzer:
# Анализировать размер APK
./gradlew :app:assembleRelease
# Получить разбивку по размеру
bundletool dump manifest --bundle=app-release.aab
bundletool get-size total --bundle=app-release.aab
Автоматизированное тестирование размера:
@RunWith(AndroidJUnit4::class)
class AppSizeTests {
@Test
fun verifyApkSizeWithinBudget() {
val apkFile = File("app/build/outputs/apk/release/app-release.apk")
assertTrue("Файл APK не найден", apkFile.exists())
val apkSizeMB = apkFile.length() / (1024.0 * 1024.0)
val maxSizeMB = 50.0 // Бюджет 50 МБ
assertTrue(
"Размер APK (${apkSizeMB}MB) превышает бюджет (${maxSizeMB}MB)",
apkSizeMB <= maxSizeMB
)
}
@Test
fun verifyNoUnusedResources() {
// Обычно это интегрируется в процесс сборки
val lintResults = runLintCheck()
val unusedResources = lintResults
.filter { it.id == "UnusedResources" }
assertTrue(
"Найдено ${unusedResources.size} неиспользуемых ресурсов",
unusedResources.isEmpty()
)
}
}
Оптимизация времени запуска
Время запуска критично для первого впечатления. Современные стратегии мобильного тестирования подчеркивают важность быстрого и отзывчивого запуска приложений на всех устройствах и версиях ОС.
Измерение времени запуска приложения
Тестирование времени запуска в iOS:
class LaunchTimeTests: XCTestCase {
func testColdLaunchTime() {
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
func testWarmLaunchTime() {
let app = XCUIApplication()
app.launch()
// Отправить в фон
XCUIDevice.shared.press(.home)
Thread.sleep(forTimeInterval: 2)
// Перезапустить
measure(metrics: [XCTApplicationLaunchMetric()]) {
app.activate()
}
}
}
Бенчмаркинг запуска в Android:
@RunWith(AndroidJUnit4::class)
class StartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun coldStartup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD
) {
pressHome()
startActivityAndWait()
}
@Test
fun warmStartup() = benchmarkRule.measureRepeated(
packageName = "com.example.app",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.WARM
) {
pressHome()
startActivityAndWait()
}
}
Заключение
Профилирование производительности мобильных приложений необходимо для создания высококачественных приложений, которые любят пользователи. Внедряя комплексные стратегии тестирования для управления памятью, потребления батареи, эффективности сети, размера приложения и времени запуска, команды могут выявлять и исправлять проблемы производительности до того, как они повлияют на пользователей.
Ключевые выводы:
- Утечки памяти должны обнаруживаться и исправляться через автоматизированное тестирование и инструменты профилирования
- Потребление батареи должно мониториться, чтобы приложения не разряжали батарею устройства чрезмерно
- Оптимизация сети снижает использование данных и улучшает отзывчивость
- Размер приложения напрямую влияет на конверсию загрузок и удовлетворенность пользователей
- Время запуска критично для первого впечатления и удержания пользователей
Тестирование производительности должно интегрироваться на протяжении всего жизненного цикла разработки, от юнит-тестов до мониторинга в продакшене, обеспечивая стабильно отличный пользовательский опыт мобильных приложений.