Pull to refresh

«65К методов хватит всем» или как бороться с лимитом DEX методов в Android

Reading time 6 min
Views 34K
Это произошло внезапно. Только что вы писали код для своего приложения под андроид, вам это нравилось, и вы наслаждались процессом. Вы добавили крутую библиотеку чтобы получить дополнительные возможности и писать более простой код. Но вместо работающего приложения на выходе вы получаете ужасающую надпись:

Unable to execute dex: method ID not in [0, 0xffff]: 65536
Conversion to Dalvik format failed: Unable to execute dex: method ID not in [0, 0xffff]: 65536

И вы в ступоре, вы неспособны создать DEX файл для APK. Вы не имеете ни малейшего представления о том, что это и как это исправить. И что бы вы не делали, оно будет приводить вас к самому логичному состоянию: ПАНИКА.

Знакомимся с врагом


После того, как вы попробовали все свои обычные трюки (в следующем порядке: чистка проекта, перезапуск IDE, сборка через командную строку, перезагрузка ноутбука, звонок другу и даже десятиминутный перерыв в надежде, что проблема исчезнет сама собой), вы вынуждены признать жестокую правду: проблема все еще здесь. Поэтому было бы неплохо узнать, что стало ее причиной.

В интернете множество постов об этой проблеме: вот один из них, еще один, и еще один; о, а так-же этот и этот тоже. Так что же произошло? По существу, выглядит, что вы столкнулись с чем-то, что часто (и ошибочно) называют Dalvik 65K methods limit. Кратко (спасибо fadden):

Вы можете ссылаться на очень большое число методов в DEX файле, но вызывать можете только первые 65536, потому что это вся память, которая у вас есть для инструкции вызова метода.
[...] ограничено число методов на которые вы можете сослаться, а не число определенных вами методов. Другими словами, если ваш DEX файл содержит всего несколько методов, но вместе они вызывают 70 000 различных внешне-определенных методов — вы превысите лимит.

Что мы имеем? У вашего приложения слишком много методов, написанных вами или заключенных в библиотечных JAR файлах. По этой причине dx tool не может записать адреса некоторых методов просто потому, что они не помещаются в определенное для этого поле в DEX файле (который в свою очередь содержит скомпилированные Java классы). И именно поэтому проблема должна называться DEX 65K methods limit.

И да, вы все правильно поняли. Эта проблема не исчезнет даже когда андроид перейдет на новый ART рантайм, до тех пор пока Google не решит “поправить” формат DEX или не сменит его на что-то другое.



Давайте посчитаем


Вы наверное удивлены, как вы умудрились зарыть больше 65 000 методов в вашем драгоценном APK. А может и нет. В любом случае, должен быть способ для подсчета этих методов и определения их источника.

Заголовок DEX файла уже содержит информацию о числе ссылок на методы (для ясности, это число представляет уникальные ссылки на методы, а не сумму вызовов для каждого метода). Но мы также хотим видеть, какой пакет добавляет непомерное число этих методов.

Лучший, найденный мной инструмент, был сделан Mihai Parparita, который написал простой bash скрипт, называющийся dex-method-counts. Скрипт очень шустрый и на выходе дает XML, который сопоставляет имя пакета с числом его методов. Другое решение принадлежит Jake Wharton, оно дает точно такой же результат, но требует намного больше времени из-за своей рекурсивной природы (Jake также написал интереснейшую статью по этой теме).

Теперь у нас в руках есть хороший инструмент, так почему бы нам не проверить его на небольшом тестовом приложении? Назовем его SampleApp. Вот код, который содержится в нашей простой Activity:

package com.whyme.example;

import android.app.Activity;
import android.os.Bundle;

public class SampleActivity extends Activity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    dummyMethod();
  }

  private void dummyMethod() {
    System.out.println(“Oh gosh.”);
  }
}


Я также включил в приложение Google Play Services 5.0.77, хотя я совсем не использую эту библиотеку. Смысл моего поступка прояснится несколькими строками ниже. Теперь давайте проанализируем вывод dex-method-counts (сжатый, для простоты):

Read in 20332 method IDs.
<root>: 20332
   : 2
   android: 834
     […]
   com: 18802
     google: 18788
       […]
     whyme: 14
       example: 14
   dalvik: 2
   system: 2
   java: 624
     […]
   javax: 5
     […]
   org: 63
     apache: 24
       […]
     json: 39


Подождите. ЧТО? Я всего лишь определил один метод в моей Activity и уже имею 20 000 методов в моем DEX файле?



Виновник очевиден.

Google Play Services: смешанные чувства


Есть ненулевая вероятность что вы знакомы с Google Play Services. Умный ход Google для поддержки API множества своих сервисов, вплоть до Android 2.3 Gingerbread, с новым релизом каждые шесть недель.

Однако все имеет свою цену, которая в данном случае явилась в виде ОГРОМНОГО числа методов которые Play Services несет в себе. Мы говорим приблизительно о 19 000 методов. И при лимите в 65536, это означает, что треть от всех методов, которые мы можем включить, уже исчерпана. Вот так.

Теперь, прежде чем мы перейдем к “решению” этой проблемы, я думаю будут уместны небольшие размышления. Некоторые разработчики как внутри, так и вне Google, разделяют общую точку зрения, которая гласит: “Если вы сталкиваетесь с ограничением, то вы плохой, очень плохой разработчик, вы заслуживаете наказания (а также вы должны гореть в аду)”. С более беспристрастной точки зрения это значит, что вы опрометчиво включили слишком много библиотек, то ли потому что вы слишком ленивы, то ли потому что ваше приложение делает слишком много. Лично я не разделяю эту точку зрения. Я считаю, что это просто оправдания для двух простых вещей:

1. Я не хочу признавать проблему / я не хочу исправлять ее.
2. Я не вышел за лимит, значит я крутой разработчик, а ты делаешь что-то не так.

Может быть, лимит был установлен из следующих соображений: “Этот лимит будет определять грань между хорошим и плохим программистом для всех будущих поколений. Да будет так.” Но что-то я в этом сомневаюсь.

Выбросьте все ненужное


В мире где справедливость торжествует, Google осознал глубину проблемы и решил сократить ущерб, который она наносит, разделив гигантский Play Services на модули, которые могут быть включены в зависимости от функциональности, которую использует ваше приложение. Например, если мое приложение не имеет отношения к играм, совершенно не имеет смысла включать в него все Play Games API. Это наиболее разумный ход, который можно предпринять и который возможно осуществить.

Однако Google не понимает в полной мере проблему, так что лучше мы найдем другой путь решения самостоятельно. Как было сказано, мы будем выбрасывать части google-play-services.jar, те части API, которые нам точно не понадобятся.

Для этого есть пара способов. Вы можете делать это вручную, но помните, что вам придется делать это каждые шесть недель.

Вы можете положиться на библиотеку JarJar, которая позволяет удалять желаемые пакеты из jar файла. Ясно и просто.

Если же вы, как и я, предпочитаете старую добрую командную строку, то вы можете использовать скрипт, который я использую для своих проектов, который называется (вы не поверите) strip_play_services.sh. Управлять работой скрипта можно с помощью конфигурационного файла strip.conf, который идет в комплекте. С помощью него вы можете выбрать модули Play Services Library, которые следует отключить. По существу скрипт делает следующее:

1. Извлекает контент из файла google-play-services.jar
2. Проверяет существует ли strip.conf, и если существует, то использует его для конфигурации, если же нет, то создает новый, записывая туда все модули из Play Services Library
3. Основываясь на strip.conf, удаляет ненужные модули из библиотеки
4. Перепаковывает оставшиеся модули в google-play-services-STRIPPED.jar файл

Вот простая конфигурация файла strip.conf:

actions=true
ads=true
analytics=true
appindexing=true
appstate=true
auth=true
cast=true
common=true
drive=false
dynamic=true
games=false
gcm=true
identity=true
internal=true
location=false
maps=false
panorama=false
plus=true
security=true
tagmanager=true
wallet=false
wearable=true


И все. Теперь для примера давайте посчитаем число методов нашего тестового приложения еще раз и посмотрим, что изменилось:

Read in 11216 method IDs.
<root>: 11216
  [...]


Это уже что-то. Подбирая подходящую конфигурацию, основываясь на нуждах вашего приложения, вы можете управлять “толщиной” Google Play Services и оставлять больше места для методов, которые вам (возможно) действительно нужны.

Но вы также должны быть очень осторожны. Play Services Library собрана очень хорошо (в плане модульности), и вы можете безопасно удалить модуль “games” не затронув при этом модуль “maps”. Но если вы удалите модуль, который используется другими (внутренний, проще говоря), то ничего не будет работать. Используйте метод проб и ошибок!



Есть ли другой путь?


Конечно есть. Запуск ProGuard для вашего приложения может помочь, так как он удаляет ненужные методы из вашего кода и в дополнение уменьшает размер APK. Но не ожидайте большого сокращения, этого не произойдет.

Другое решение заключается в создании второго DEX файла, который содержит часть вашего кода, доступ к которому вы будете осуществлять через интерфейсы/reflections и который вы будете грузить через кастомный ClassLoader (подробнее). Это решение может быть более эффективным в некоторых случаях. Однако этот путь очень нетривиален.

Я нашел еще одно решение моего хорошего друга Dario Marcato. Он создал Gradle task для удаления ненужных модулей. Если вы используете Gradle, то обязательно попробуйте!.

Послесловие


Это перевод оригинальной статьи Sebastiano Gottardo, [DEX] Sky’s the limit? No, 65K methods is. Если перевод кажется вам «корявым» или вы нашли орфографические/синтаксические/фактические ошибки в тексте, пожалуйста, напишите мне в ЛС.
Tags:
Hubs:
+51
Comments 7
Comments Comments 7

Articles