android.os.TransactionTooLargeException на нуге

Я обновил Nexus 5X до Android N, и теперь, когда я устанавливаю на него приложение (отладку или выпуск), я получаю исключение TransactionTooLargeException при каждом переходе экрана с Bundle в дополнениях. Приложение работает на всех других устройствах. Старое приложение, которое находится в PlayStore и имеет в основном тот же код, работает на Nexus 5X. У кого-нибудь есть такая же проблема?

java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 592196 bytes
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3752)
   at android.os.Handler.handleCallback(Handler.java:751)
   at android.os.Handler.dispatchMessage(Handler.java:95)
   at android.os.Looper.loop(Looper.java:154)
   at android.app.ActivityThread.main(ActivityThread.java:6077)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755)
Caused by: android.os.TransactionTooLargeException: data parcel size 592196 bytes
   at android.os.BinderProxy.transactNative(Native Method)
   at android.os.BinderProxy.transact(Binder.java:615)
   at android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3606)
   at android.app.ActivityThread$StopInfo.run(ActivityThread.java:3744)
   at android.os.Handler.handleCallback(Handler.java:751) 
   at android.os.Handler.dispatchMessage(Handler.java:95) 
   at android.os.Looper.loop(Looper.java:154) 
   at android.app.ActivityThread.main(ActivityThread.java:6077) 
   at java.lang.reflect.Method.invoke(Native Method) 
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:865) 
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:755) 

12 ответов

Решение

В конце моя проблема была с вещами, которые были сохранены в SaveInstance, а не с вещами, которые были отправлены к следующему действию. Я удалил все сохранения, где я не могу контролировать размер объектов (ответы по сети), и теперь это работает.

Обновить:

Чтобы сохранить большие порции данных, Google предлагает сделать это с фрагментом, который сохраняет экземпляр. Идея состоит в том, чтобы создать пустой фрагмент без представления со всеми необходимыми полями, которые в противном случае были бы сохранены в Bundle. добавлять setRetainInstance(true); к методу onCreate фрагмента. А затем сохраните данные во Fragment on onDestroy Activity и загрузите их на Create. Вот пример действия:

public class MyActivity extends Activity {

    private DataFragment dataFragment;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        // find the retained fragment on activity restarts
        FragmentManager fm = getFragmentManager();
        dataFragment = (DataFragment) fm.findFragmentByTag(“data”);

        // create the fragment and data the first time
        if (dataFragment == null) {
            // add the fragment
            dataFragment = new DataFragment();
            fm.beginTransaction().add(dataFragment, “data”).commit();
            // load the data from the web
            dataFragment.setData(loadMyData());
        }

        // the data is available in dataFragment.getData()
        ...
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // store the data in the fragment
        dataFragment.setData(collectMyLoadedData());
    }
}

И пример фрагмента:

public class DataFragment extends Fragment {

    // data object we want to retain
    private MyDataObject data;

    // this method is only called once for this fragment
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // retain this fragment
        setRetainInstance(true);
    }

    public void setData(MyDataObject data) {
        this.data = data;
    }

    public MyDataObject getData() {
        return data;
    }
}

Подробнее об этом вы можете прочитать здесь.

Всякий раз, когда вы видите TransactionTooLargeException происходит, когда Activity находится в процессе остановки, это означает, что Activity пытался отправить свое сохраненное состояние Bundles к операционной системе системы для безопасного хранения для восстановления позже (после изменения конфигурации или смерти процесса), но один или несколько из Bundles оно было слишком большим. Для всех таких транзакций, которые происходят одновременно, максимальный лимит составляет около 1 МБ, и этот лимит может быть достигнут, даже если ни один Bundle превышает этот предел.

Основным виновником здесь обычно является сохранение слишком большого количества данных внутри onSaveInstanceState либо из Activity или любой Fragments размещенный Activity, Обычно это происходит при сохранении чего-то особенно большого, например Bitmap но это также может произойти при отправке большого количества небольших данных, таких как списки Parcelable объекты. Команда Android неоднократно ясно давала понять, что только небольшие объемы данных, связанных с просмотром, следует сохранять в onSavedInstanceState, Тем не менее, разработчики часто сохраняют страницы сетевых данных, чтобы изменения конфигурации выглядели как можно более плавными, так как больше не нужно повторно выбирать те же данные. Начиная с Google I/O 2017, команда Android ясно дала понять, что предпочтительная архитектура для приложения Android сохраняет сетевые данные.

  • в памяти, так что он может быть легко использован при изменении конфигурации
  • на диск, чтобы его можно было легко восстановить после смерти процесса и приложений

Их новый ViewModel рамки и Room Постоянная библиотека предназначена для того, чтобы помочь разработчикам соответствовать этому шаблону. Если ваша проблема заключается в сохранении слишком большого количества данных в onSaveInstanceStateОбновление до такой архитектуры с помощью этих инструментов должно решить вашу проблему.

Лично, прежде чем перейти к этому новому шаблону, я хотел бы взять мои существующие приложения и просто обойти TransactionTooLargeException в это время. Я написал небольшую библиотеку для этого: https://github.com/livefront/bridge. Он использует те же общие идеи восстановления состояния из памяти при изменениях конфигурации и с диска после смерти процесса, а не отправляет все это состояние в ОС через onSaveInstanceState, но требует очень минимальных изменений в вашем существующем коде для использования. Любая стратегия, которая соответствует этим двум целям, должна помочь вам избежать исключения, не жертвуя своей способностью сохранять состояние.

И последнее замечание: единственная причина, по которой вы видите это на Nougat +, заключается в том, что изначально, если предел транзакции подшивки был превышен, процесс отправки сохраненного состояния в ОС завершился бы молча, и в Logcat появилась только эта ошибка:

!!! СБОЙ СДЕЛКИ БИНДЕРА!!!

В Нуге этот тихий сбой был превращен в серьезный сбой. К их чести, это то, что команда разработчиков задокументировала в примечаниях к выпуску для Nougat:

Многие API платформы теперь начали проверять большие полезные нагрузки, отправляемые через транзакции Binder, и система теперь перебрасывает TransactionTooLargeExceptions как RuntimeExceptions, вместо того, чтобы молча регистрировать или подавлять их. Одним из распространенных примеров является хранение слишком большого количества данных в Activity.onSaveInstanceState(), в результате чего ActivityThread.StopInfo выдает исключение RuntimeException, когда ваше приложение предназначено для Android 7.0.

Сделал удар и испытание, и, наконец, это решило мою проблему. Добавьте это к вашему Activity

@Override
protected void onSaveInstanceState(Bundle oldInstanceState) {
    super.onSaveInstanceState(oldInstanceState);
    oldInstanceState.clear();
}

TransactionTooLargeException преследует нас уже около 4 месяцев, и мы наконец решили проблему!

То, что происходило, было то, что мы используем FragmentStatePagerAdapter в ViewPager. Пользователь будет пролистывать и создавать более 100 фрагментов (это приложение для чтения).

Хотя мы должным образом управляем фрагментами в destroyItem(), в реализации FragmentStatePagerAdapter в Android есть ошибка, в которой хранится ссылка на следующий список:

private ArrayList<Fragment.SavedState> mSavedState = new ArrayList<Fragment.SavedState>();

И когда FragmentStatePagerAdapter Android пытается сохранить состояние, он вызовет функцию

@Override
public Parcelable saveState() {
    Bundle state = null;
    if (mSavedState.size() > 0) {
        state = new Bundle();
        Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()];
        mSavedState.toArray(fss);
        state.putParcelableArray("states", fss);
    }
    for (int i=0; i<mFragments.size(); i++) {
        Fragment f = mFragments.get(i);
        if (f != null && f.isAdded()) {
            if (state == null) {
                state = new Bundle();
            }
            String key = "f" + i;
            mFragmentManager.putFragment(state, key, f);
        }
    }
    return state;
}

Как вы можете видеть, даже если вы правильно управляете фрагментами в подклассе FragmentStatePagerAdapter, базовый класс все равно будет хранить Fragment.SavedState для каждого отдельного когда-либо созданного фрагмента. TransactionTooLargeException возникнет, когда этот массив будет выгружен в массив parcelableArray, и ОС не понравится более 100 элементов.

Поэтому исправление для нас состояло в том, чтобы переопределить метод saveState() и ничего не хранить для "состояний".

@Override
public Parcelable saveState() {
    Bundle bundle = (Bundle) super.saveState();
    bundle.putParcelableArray("states", null); // Never maintain any states from the base class, just null it out
    return bundle;
}

Я сталкиваюсь с этой проблемой также на моих устройствах Nougat. Мое приложение использует фрагмент с пейджером просмотра, который содержит 4 фрагмента. Я передал несколько больших аргументов конструкции 4 фрагментам, которые вызвали проблему.

Я проследил размер Bundle вызывая это с помощью TooLargeTool.

Наконец, я решил это с помощью putSerializable на объекте POJO, который реализует Serializable вместо прохождения большого сырья String с помощью putString во время инициализации фрагмента. Это уменьшенный размер Bundle наполовину и не бросает TransactionTooLargeException, Поэтому, пожалуйста, убедитесь, что вы не передаете аргументы огромного размера Fragment,

Проблема PS связана с системой отслеживания проблем Google: https://issuetracker.google.com/issues/37103380

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

Сценарий: я получил странную ошибку от клиента на устройстве Google Nexus 6P (7 ОС), так как мое приложение зависло после 4 часов работы. Позже я обнаружил, что он вызывает похожее исключение (android.os.TransactionTooLargeException:).

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

 @Override
    public void onBackStackChanged() {
        try {
            int count = mFragmentMngr.getBackStackEntryCount();
            if (count > 0) {
                if (count > 30) {
                    mFragmentMngr.popBackStack(1, FragmentManager.POP_BACK_STACK_INCLUSIVE);
                    count = mFragmentMngr.getBackStackEntryCount();
                }
                FragmentManager.BackStackEntry entry = mFragmentMngr.getBackStackEntryAt(count - 1);
                mCurrentlyLoadedFragment = Integer.parseInt(entry.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

В моем случае я получил это исключение внутри фрагмента, потому что одним из его аргументов была очень большая строка, которую я забыл удалить (я использовал только эту большую строку внутри метода onViewCreated()). Итак, чтобы решить это, я просто удалил этот аргумент. В вашем случае вы должны очистить или аннулировать любое подозрительное поле перед вызовом onPause().

Код активности

Fragment fragment = new Fragment();
Bundle args = new Bundle();
args.putString("extremely large string", data.getValue());
fragment.setArguments(args);

Код фрагмента

@Override 
public void onViewCreated(View view, Bundle savedInstanceState) {

    String largeString = arguments.get("extremely large string");       
    //Do Something with the large string   
    arguments.clear() //I forgot to execute this  
}

Проблема в моем приложении заключалась в том, что я пытался сохранить слишком много в saveInstanceState, решение было точно определить, какие данные должны быть сохранены в нужное время. В основном внимательно посмотрите на ваш onSaveInstanceState, чтобы убедиться, что вы не растягиваете его:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current state
    // Check carefully what you're adding into the savedInstanceState before saving it
    super.onSaveInstanceState(savedInstanceState);
}

В моем случае я использовал TooLargeTool, чтобы отследить, откуда возникла проблема, и узнал android:support:fragments ключ в Bundle от моего onSaveInstanceState раньше достигал почти 1 Мб, когда приложение падало. Таким образом, решение было похоже на:

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.remove("android:support:fragments");
}

Делая это, я избегал сохранять состояния всех фрагментов и сохранял другие вещи, которые должны быть сохранены.

Ни один из приведенных выше ответов для меня не сработал, причина проблемы была довольно простой, как утверждают некоторые, я использовал FragmentStatePagerAdapter, а его метод saveState сохраняет состояние фрагментов, потому что один из моих фрагментов был довольно большим, поэтому сохранение этого фрагмента приводит к этому TransactionTooLargeExecption.

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

У моего фрагмента был EditText, который содержал очень большой текст, который был причиной проблемы в моем случае, поэтому просто в onPause() фрагмента я установил текст edittext в пустую строку. то есть:

@Override
    public void onPause() {
       edittext.setText("");
}

Теперь, когда FragmentStatePagerAdapter попытается сохранить State, этот большой фрагмент текста не будет там, чтобы потреблять большую его часть, следовательно, устраняет сбой.

В вашем случае вам нужно найти любого виновника, это может быть ImageView с некоторым растровым изображением, TextView с огромным фрагментом текста или любое другое представление с высоким потреблением памяти, вам нужно освободить его память, вы можете установить imageview.setImageResource(null) или аналогичный в onPause() вашего фрагмента.

update: onSaveInstanceState - лучшее место для этой цели перед вызовом super, например:

@Override
    public void onSaveInstanceState(Bundle outState) {
        edittext.setText("");
        super.onSaveInstanceState(outState);
    }

или, как указано @Vladimir, вы можете использовать android:saveEnabled="false" или view.setSaveEnabled(false); в представлении или пользовательском представлении и убедитесь, что текст снова задан в onResume, иначе он будет пустым при возобновлении действия.

Просто переопределите этот метод на вашей активности:

@Override
protected void onSaveInstanceState(Bundle outState) {
    // below line to be commented to prevent crash on nougat.
    // http://blog.sqisland.com/2016/09/transactiontoolargeexception-crashes-nougat.html
    //
    //super.onSaveInstanceState(outState);
}

Перейдите на https://code.google.com/p/android/issues/detail?id=212316 для получения дополнительной информации.

Я столкнулся с той же проблемой. Мой обходной путь разгружает сохраненный экземпляр состояния в файлы в директории кеша.

Я сделал следующий полезный класс.

package net.cattaka.android.snippets.issue;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 * To parry BUG of Android N. https://code.google.com/p/android/issues/detail?id=212316
 * <p>
 * Created by cattaka on 2017/01/12.
 */
public class Issue212316Parrier {
    public static final String DEFAULT_NAME = "Issue212316Parrier";
    private static final String KEY_STORED_BUNDLE_ID = "net.cattaka.android.snippets.issue.Issue212316Parrier.KEY_STORED_BUNDLE_ID";

    private String mName;
    private Context mContext;
    private String mAppVersionName;
    private int mAppVersionCode;
    private SharedPreferences mPreferences;
    private File mDirForStoredBundle;

    public Issue212316Parrier(Context context, String appVersionName, int appVersionCode) {
        this(context, appVersionName, appVersionCode, DEFAULT_NAME);
    }

    public Issue212316Parrier(Context context, String appVersionName, int appVersionCode, String name) {
        mName = name;
        mContext = context;
        mAppVersionName = appVersionName;
        mAppVersionCode = appVersionCode;
    }

    public void initialize() {
        mPreferences = mContext.getSharedPreferences(mName, Context.MODE_PRIVATE);

        File cacheDir = mContext.getCacheDir();
        mDirForStoredBundle = new File(cacheDir, mName);
        if (!mDirForStoredBundle.exists()) {
            mDirForStoredBundle.mkdirs();
        }

        long lastStoredBundleId = 1;
        boolean needReset = true;
        String fingerPrint = (Build.FINGERPRINT != null) ? Build.FINGERPRINT : "";
        needReset = !fingerPrint.equals(mPreferences.getString("deviceFingerprint", null))
                || !mAppVersionName.equals(mPreferences.getString("appVersionName", null))
                || (mAppVersionCode != mPreferences.getInt("appVersionCode", 0));
        lastStoredBundleId = mPreferences.getLong("lastStoredBundleId", 1);

        if (needReset) {
            clearDirForStoredBundle();

            mPreferences.edit()
                    .putString("deviceFingerprint", Build.FINGERPRINT)
                    .putString("appVersionName", mAppVersionName)
                    .putInt("appVersionCode", mAppVersionCode)
                    .putLong("lastStoredBundleId", lastStoredBundleId)
                    .apply();
        }
    }

    /**
     * Call this from {@link android.app.Activity#onCreate(Bundle)}, {@link android.app.Activity#onRestoreInstanceState(Bundle)} or {@link android.app.Activity#onPostCreate(Bundle)}
     */
    public void restoreSaveInstanceState(@Nullable Bundle savedInstanceState, boolean deleteStoredBundle) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_BUNDLE_ID)) {
                long storedBundleId = savedInstanceState.getLong(KEY_STORED_BUNDLE_ID);
                File storedBundleFile = new File(mDirForStoredBundle, storedBundleId + ".bin");
                Bundle storedBundle = loadBundle(storedBundleFile);
                if (storedBundle != null) {
                    savedInstanceState.putAll(storedBundle);
                }
                if (deleteStoredBundle && storedBundleFile.exists()) {
                    storedBundleFile.delete();
                }
            }
        }
    }

    /**
     * Call this from {@link android.app.Activity#onSaveInstanceState(Bundle)}
     */
    public void saveInstanceState(Bundle outState) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            if (outState != null) {
                long nextStoredBundleId = mPreferences.getLong("lastStoredBundleId", 1) + 1;
                mPreferences.edit().putLong("lastStoredBundleId", nextStoredBundleId).apply();
                File storedBundleFile = new File(mDirForStoredBundle, nextStoredBundleId + ".bin");
                saveBundle(outState, storedBundleFile);
                outState.clear();
                outState.putLong(KEY_STORED_BUNDLE_ID, nextStoredBundleId);
            }
        }
    }

    private void saveBundle(@NonNull Bundle bundle, @NonNull File storedBundleFile) {
        byte[] blob = marshall(bundle);
        OutputStream out = null;
        try {
            out = new GZIPOutputStream(new FileOutputStream(storedBundleFile));
            out.write(blob);
            out.flush();
            out.close();
        } catch (IOException e) {
            // ignore
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }
    }

    @Nullable
    private Bundle loadBundle(File storedBundleFile) {
        byte[] blob = null;
        InputStream in = null;
        try {
            in = new GZIPInputStream(new FileInputStream(storedBundleFile));
            ByteArrayOutputStream bout = new ByteArrayOutputStream();
            int n;
            byte[] buffer = new byte[1024];
            while ((n = in.read(buffer)) > -1) {
                bout.write(buffer, 0, n);   // Don't allow any extra bytes to creep in, final write
            }
            bout.close();
            blob = bout.toByteArray();
        } catch (IOException e) {
            // ignore
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    // ignore
                }
            }
        }

        try {
            return (blob != null) ? (Bundle) unmarshall(blob) : null;
        } catch (Exception e) {
            return null;
        }
    }

    private void clearDirForStoredBundle() {
        for (File file : mDirForStoredBundle.listFiles()) {
            if (file.isFile() && file.getName().endsWith(".bin")) {
                file.delete();
            }
        }
    }


    @NonNull
    private static <T extends Parcelable> byte[] marshall(@NonNull final T object) {
        Parcel p1 = Parcel.obtain();
        p1.writeValue(object);

        byte[] data = p1.marshall();
        p1.recycle();
        return data;
    }

    @SuppressWarnings("unchecked")
    @NonNull
    private static <T extends Parcelable> T unmarshall(@NonNull byte[] bytes) {
        Parcel p2 = Parcel.obtain();
        p2.unmarshall(bytes, 0, bytes.length);
        p2.setDataPosition(0);
        T result = (T) p2.readValue(Issue212316Parrier.class.getClassLoader());
        p2.recycle();
        return result;
    }
}

Полные коды: https://github.com/cattaka/AndroidSnippets/pull/37

Я беспокоюсь о том, что Parcel # marshall не следует использовать для постоянного хранения. Но у меня нет другой идеи.

Поскольку Android N меняет поведение и генерирует TransactionTooLargeException вместо регистрации ошибки.

     try {
            if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity);
            ActivityManagerNative.getDefault().activityStopped(
                activity.token, state, persistentState, description);
        } catch (RemoteException ex) {
            if (ex instanceof TransactionTooLargeException
                    && activity.packageInfo.getTargetSdkVersion() < Build.VERSION_CODES.N) {
                Log.e(TAG, "App sent too much data in instance state, so it was ignored", ex);
                return;
            }
            throw ex.rethrowFromSystemServer();
        }

Мое решение состоит в том, чтобы подключить экземпляр ActivityMangerProxy и попытаться поймать метод ActivityStopped.

Вот код:

private boolean hookActivityManagerNative() {
    try {
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Field singletonField = ReflectUtils.findField(loader.loadClass("android.app.ActivityManagerNative"), "gDefault");
        ReflectUtils.ReflectObject singletonObjWrap = ReflectUtils.wrap(singletonField.get(null));
        Object realActivityManager = singletonObjWrap.getChildField("mInstance").get();
        Object fakeActivityManager = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
                new Class[]{loader.loadClass("android.app.IActivityManager")}, new ActivityManagerHook(realActivityManager));
        singletonObjWrap.setChildField("mInstance", fakeActivityManager);
        return true;
    } catch (Throwable e) {
        AppHolder.getThirdPartUtils().markException(e);
        return false;
    }
}

private static class ActivityManagerHook implements InvocationHandler {

    private Object origin;

    ActivityManagerHook(Object origin) {
       this.origin = origin;
    }

    public Object getOrigin() {
        return origin;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        switch (method.getName()) {
            //ActivityManagerNative.getDefault().activityStopped(activity.token, state, persistentState, description);
            case "activityStopped": {
                try {
                    return method.invoke(getOrigin(), args);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return null;
            }
        }
        return method.invoke(getOrigin(), args);
    }
}

И класс вспомогательного отражения

public class ReflectUtils {

private static final HashMap<String, Field> fieldCache = new HashMap<>();
private static final HashMap<String, Method> methodCache = new HashMap<>();

public static Field findField(Class<?> clazz, String fieldName) throws Throwable {
    String fullFieldName = clazz.getName() + '#' + fieldName;

    if (fieldCache.containsKey(fullFieldName)) {
        Field field = fieldCache.get(fullFieldName);
        if (field == null)
            throw new NoSuchFieldError(fullFieldName);
        return field;
    }

    try {
        Field field = findFieldRecursiveImpl(clazz, fieldName);
        field.setAccessible(true);
        fieldCache.put(fullFieldName, field);
        return field;
    } catch (NoSuchFieldException e) {
        fieldCache.put(fullFieldName, null);
        throw new NoSuchFieldError(fullFieldName);
    }
}


private static Field findFieldRecursiveImpl(Class<?> clazz, String fieldName) throws NoSuchFieldException {
    try {
        return clazz.getDeclaredField(fieldName);
    } catch (NoSuchFieldException e) {
        while (true) {
            clazz = clazz.getSuperclass();
            if (clazz == null || clazz.equals(Object.class))
                break;

            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException ignored) {
            }
        }
        throw e;
    }
}


public static Method findMethodExact(Class<?> clazz, String methodName, Class<?>... parameterTypes) throws Throwable {
    String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

    if (methodCache.containsKey(fullMethodName)) {
        Method method = methodCache.get(fullMethodName);
        if (method == null)
            throw new NoSuchMethodError(fullMethodName);
        return method;
    }

    try {
        Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
        method.setAccessible(true);
        methodCache.put(fullMethodName, method);
        return method;
    } catch (NoSuchMethodException e) {
        methodCache.put(fullMethodName, null);
        throw new NoSuchMethodError(fullMethodName);
    }
}


/**
 * Returns an array of the given classes.
 */
public static Class<?>[] getClassesAsArray(Class<?>... clazzes) {
    return clazzes;
}

private static String getParametersString(Class<?>... clazzes) {
    StringBuilder sb = new StringBuilder("(");
    boolean first = true;
    for (Class<?> clazz : clazzes) {
        if (first)
            first = false;
        else
            sb.append(",");

        if (clazz != null)
            sb.append(clazz.getCanonicalName());
        else
            sb.append("null");
    }
    sb.append(")");
    return sb.toString();
}

/**
 * Retrieve classes from an array, where each element might either be a Class
 * already, or a String with the full class name.
 */
private static Class<?>[] getParameterClasses(ClassLoader classLoader, Object[] parameterTypes) throws ClassNotFoundException {
    Class<?>[] parameterClasses = null;
    for (int i = parameterTypes.length - 1; i >= 0; i--) {
        Object type = parameterTypes[i];
        if (type == null)
            throw new ClassNotFoundException("parameter type must not be null", null);

        if (parameterClasses == null)
            parameterClasses = new Class<?>[i + 1];

        if (type instanceof Class)
            parameterClasses[i] = (Class<?>) type;
        else if (type instanceof String)
            parameterClasses[i] = findClass((String) type, classLoader);
        else
            throw new ClassNotFoundException("parameter type must either be specified as Class or String", null);
    }

    // if there are no arguments for the method
    if (parameterClasses == null)
        parameterClasses = new Class<?>[0];

    return parameterClasses;
}

public static Class<?> findClass(String className, ClassLoader classLoader) throws ClassNotFoundException {
    if (classLoader == null)
        classLoader = ClassLoader.getSystemClassLoader();
    return classLoader.loadClass(className);
}


public static ReflectObject wrap(Object object) {
    return new ReflectObject(object);
}


public static class ReflectObject {

    private Object object;

    private ReflectObject(Object o) {
        this.object = o;
    }

    public ReflectObject getChildField(String fieldName) throws Throwable {
        Object child = ReflectUtils.findField(object.getClass(), fieldName).get(object);
        return ReflectUtils.wrap(child);
    }

    public void setChildField(String fieldName, Object o) throws Throwable {
        ReflectUtils.findField(object.getClass(), fieldName).set(object, o);
    }

    public ReflectObject callMethod(String methodName, Object... args) throws Throwable {
        Class<?>[] clazzs = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            clazzs[i] = args.getClass();
        }
        Method method = ReflectUtils.findMethodExact(object.getClass(), methodName, clazzs);
        return ReflectUtils.wrap(method.invoke(object, args));
    }

    public <T> T getAs(Class<T> clazz) {
        return (T) object;
    }

    public <T> T get() {
        return (T) object;
    }
}
}
Другие вопросы по тегам