Tuesday, March 8, 2011

Ошибка транзакций в приложениях

Мутантская ошибка, которая может послужить причиной длительного брейнфакерства.

Приложение для Андроид (Froyo, но также воспроизводится и на Gingerbread, т.е. встречается на API версий 8-9), по событию читает страницу в интернете и отображает ее в виджете. Вроде вся магия простая как в мультфильмах, но почему-то картинки в виджете в одних случаях показываются, в других - не показываются. Стали разбираться.

Проблемы начинаются после перехода на девайсы с большим разрешением экрана - это стало понятно сразу. Лог - чистый и красивый, за исключением одной строки:
JavaBinder "!!! Failed binder transaction !!!"

Ясно, что кто-то куда-то не может прицепиться, из-за чего система выкидывает ошибку и перестает апдейтить процесс. Но что куда не может прицепиться и по какой причине - оставалось загадкой.

Указанную выше ошибку выкидывает JavaBinder, у которого платформой ограничены размеры транзакций (что-то около 100 кб). Ошибка возникает, если между двумя отдельно взятыми процессами единовременно передается большой (т.е. гораздо больше размера транзакции) кусок информации. В нашем случае, колдунство предполагает создание скриншота веб-страницы в одном процессе и передача его для обновления в виджет. То есть прямо по учебнику.

Ключем к пониманию этой магии послужила смена разрешения экранов. Для того, чтобы сохранить отображаемую веб-страницу в качестве картинки, нам нужно воспользоваться контроллом WebView (переопределив его PictureListener). Получаемая на выходе картинка представляет собой загруженную и отображеную в контролле веб-страницу. Линейные размеры картинки не отличаются в зависимости от количества пукселей на дисплее девайса, однако при отображении в WebView или в виджете размер конечного файла может сильно отличаться.

Например, нам надо отобразить скриншот веб-страницы размером 600х600 пукселей на экране разрешением 300х300 (таких экранов на рынок не выпускают, это для примера). Очевидно, что в этом случае требуется вмешательство волшебства, уменьшающего реальный размер картинки в четыре раза (каждую сторону - в два раза). Понятно, что для экранов разрешением 600х600 уменьшать ничего не придется и размер картинки будет в 4 раза большим. Поэтому для относительно небольших экранов картинки удачно обрабатываются транзакциями, в то время как для больших экранов переброска объемных данных вызывает ошибки в транзакциях: виртуальная машина выбрасывает исключение и перестает обновлять процесс, от этого виджет хоть и существует в памяти, на самом деле не обновляется (в нашем случае он оставался невидимым, но это зависит только от лэйаута).

Самый простой способ предотвратить слишком большой размер файла - принудительное уменьшение размера файла:

public static Bitmap makeBitmap() {
    Bitmap bitmap = null;
    BitmapFactory.Options bmOp = new BitmapFactory.Options();
    String filePath = "foo/bar/baz";

    // Здесь указываем, во сколько раз нужно
    // уменьшить каждую сторону картинки
    bmOp.inSampleSize = 2;
    bitmap = BitmapFactory.decodeFile(filePath, bmOp);

    return bitmap;
}

NB. Рекомендую использовать четные числа для семплирования (в теоретическом разделе интернета всем всегда рады).

Однако такой колдунство не всегда прокатывает по двум причинам:
1. Не всегда понятно, во сколько раз нужно уменьшать размеры
2. Уменьшенная картинка - это уменьшенная картинка, т.е. если вы (или ваш заказчик в лице Докомо) придумали показывать такую картинку на весь экран девайса - картика будет в 2 (3, 4, 5 и т.д.) раз меньше размера экрана.

Может, причин сущетсвует еще больше (практически уверен).

Поэтому сейчас колдунство следует совместить с алхимией. Для этого нужно полученную картинку сначала явно уменьшить до какого-нибудь приемлимого значения, а потом вписать уменьшенную картинку в нужную область на Canvas'е:

public static void makeBitmap(Picture picture) {
    Context con = getApplicationContext();
    Resources r = con.getResources();
    DisplayMetrics me = r.getDisplayMetrics();

    float screenWidth = me.widthPixels;
    float screenHeight = me.heightPixels;

    float screenScale;
    Bitmap b;
    Canvas c;

    int newPicWidth, newPicHeight;

    // Смотрим, во сколько ширина дисплея больше высоты
    screenScale = screenWidth / screenHeight;
    
    // Устанавливаем размер картинки на 80% от размера экрана
    // При разрешении 854х600 ошибок переполнения буффера транзакции
    // возникать не должно, но если что - число можно подрегулировать
    newPicWidth = (int)(screenWidth * 0.8);
    newPicHeight = (int)(newPicWidth / screenScale * 0.8);
    
    // Создаем битмап и холст для него
    b = Bitmap.createBitmap(newPicWidth, newPicHeight, Bitmap.Config.RGB_565);
    c = new Canvas(b);

    Rect dst = new Rect();
    dst.top = 0;
    dst.left = 0;
    dst.right = newPicWidth;
    dst.bottom = picHeight;

    // Рисуем картинку на битмап
    // Картинка растягивается или ужимается при необходимости
    c.drawPicture(picture, dst);
    
    // После этого можем сохранить битмап как файл
    // или открыть его в виджете
}