AndroidX Fragment 탐구 - 상태 저장 및 복구

96183 단어 Android
문서 목록
  • 개요
  • 원본 탐구
  • 상태 보존
  • 상태 복구
  • commit & commitAllowingStateLoss
  • 요약
  • 개술
    Activity가 onSaveInstanceState와 onRestoreInstanceState 리셋 방법을 제공하여 상태 저장과 복구에 사용하고 마찬가지로 FragmentActivity와 FragmentActivity도 상태 저장과 복구를 지원합니다. FragmentActivity는 적당한 시기에 Fragment Manager Impl을 통해 Fragment에 저장 작업을 통지하고 다음에 원본 코드에서 이 통지 과정을 추적합니다.
    원본 코드 탐구
    글의 원본 코드는'androidx'를 기반으로 합니다.fragment:fragment:1.1.0’
    상태 저장
    FragmentActivity가 onSaveInstanceState 메서드를 다시 썼습니다. [FragmentActivity.java]
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        // ···
        //   FragmentController saveAllState    Parcelable
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            //  Parcelable   outState,FRAGMENTS_TAG  "android:support:fragments"
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        // ···
    }
    

    outState에서 FRAGMENTSTAG는 키를 위해 데이터를 저장합니다.
    Fragment Controller의 saveAllState 방법에서 Fragment Manager Impl의 saveAllState 방법을 호출합니다: [Fragment Manager Impl.java]
    Parcelable saveAllState() {
        // Make sure all pending operations have now been executed to get
        // our state update-to-date.
        forcePostponedTransactions();
        endAnimatingAwayFragments();
        execPendingActions();
    
        //        true
        mStateSaved = true;
    
        if (mActive.isEmpty()) {
            return null;
        }
    
        // First collect all active fragments.
        int size = mActive.size();
        ArrayList<FragmentState> active = new ArrayList<>(size);
        boolean haveFragments = false;
        for (Fragment f : mActive.values()) {
            if (f != null) {
                if (f.mFragmentManager != this) {
                    throwException(new IllegalStateException(
                            "Failure saving state: active " + f
                                    + " was removed from the FragmentManager"));
                }
    
                haveFragments = true;
    
                //   FragmentState    Fragment       
                FragmentState fs = new FragmentState(f);
                active.add(fs);
    
                // fs.mSavedFragmentState      
                if (f.mState > Fragment.INITIALIZING && fs.mSavedFragmentState == null) {
                    //     fragment      
                    fs.mSavedFragmentState = saveFragmentBasicState(f);
    
                    //   setTargetFragment,   TargetFragment      
                    if (f.mTargetWho != null) {
                        Fragment target = mActive.get(f.mTargetWho);
                        if (target == null) {
                            throwException(new IllegalStateException(
                                    "Failure saving state: " + f
                                            + " has target not in fragment manager: "
                                            + f.mTargetWho));
                        }
                        if (fs.mSavedFragmentState == null) {
                            fs.mSavedFragmentState = new Bundle();
                        }
                        putFragment(fs.mSavedFragmentState,
                                FragmentManagerImpl.TARGET_STATE_TAG, target);
                        if (f.mTargetRequestCode != 0) {
                            fs.mSavedFragmentState.putInt(
                                    FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG,
                                    f.mTargetRequestCode);
                        }
                    }
    
                } else {
                    fs.mSavedFragmentState = f.mSavedFragmentState;
                }
    
                if (DEBUG) Log.v(TAG, "Saved state of " + f + ": "
                        + fs.mSavedFragmentState);
            }
        }
    
        if (!haveFragments) {
            if (DEBUG) Log.v(TAG, "saveAllState: no fragments!");
            return null;
        }
    
        ArrayList<String> added = null;
        BackStackState[] backStack = null;
    
        // Build list of currently added fragments.
        //   mAdded    Fragment  ID
        size = mAdded.size();
        if (size > 0) {
            added = new ArrayList<>(size);
            for (Fragment f : mAdded) {
                // f.mWho Fragment         UUID
                added.add(f.mWho);
                if (f.mFragmentManager != this) {
                    throwException(new IllegalStateException(
                            "Failure saving state: active " + f
                                    + " was removed from the FragmentManager"));
                }
                if (DEBUG) {
                    Log.v(TAG, "saveAllState: adding fragment (" + f.mWho
                            + "): " + f);
                }
            }
        }
    
        // Now save back stack.
        //   addToBackStack,            
        if (mBackStack != null) {
            size = mBackStack.size();
            if (size > 0) {
                backStack = new BackStackState[size];
                for (int i = 0; i < size; i++) {
                    //   BackStackState    BackStackRecord   
                    backStack[i] = new BackStackState(mBackStack.get(i));
                    if (DEBUG) Log.v(TAG, "saveAllState: adding back stack #" + i
                            + ": " + mBackStack.get(i));
                }
            }
        }
    
        //   FragmentManagerState     FragmentManagerImpl    
        FragmentManagerState fms = new FragmentManagerState();
        fms.mActive = active;
        fms.mAdded = added;
        fms.mBackStack = backStack;
        if (mPrimaryNav != null) {
            fms.mPrimaryNavActiveWho = mPrimaryNav.mWho;
        }
        fms.mNextFragmentIndex = mNextFragmentIndex;
        return fms;
    }
    

    이 방법에서 Fragment State 집합을 만들어서 각각의 Fragment 데이터를 저장하고, String 집합은 각각의 Fragment의 유일한 ID를 저장하며, Back Stack State 그룹은 각각의 Back StackRecord 데이터를 저장하고, 최종적으로 Fragment Manager State를 만들어서 위의 모든 데이터를 저장하고 Bundle에 추가합니다.
    이어서 saveFragmentBasicState 방법을 보십시오. 이 방법에서 더 자세한 데이터를 저장하고 Bundle이 FragmentState에 부여한 mSavedFragmentState 구성원에게 값을 되돌려 저장합니다. [FragmentManagerImpl.java]
    Bundle saveFragmentBasicState(Fragment f) {
        Bundle result = null;
    
        if (mStateBundle == null) {
            mStateBundle = new Bundle();
        }
        //   Fragment onSaveInstanceState    ;  Fragment  Fragment      
        f.performSaveInstanceState(mStateBundle);
        // Lifecycle        
        dispatchOnFragmentSaveInstanceState(f, mStateBundle, false);
        if (!mStateBundle.isEmpty()) {
            result = mStateBundle;
            mStateBundle = null;
        }
    
        //   fragment   view,           
        if (f.mView != null) {
            saveFragmentViewState(f);
        }
        if (f.mSavedViewState != null) {
            if (result == null) {
                result = new Bundle();
            }
            //        
            result.putSparseParcelableArray(
                    FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState);
        }
        if (!f.mUserVisibleHint) {
            if (result == null) {
                result = new Bundle();
            }
            // Only add this if it's not the default value
            //       mUserVisibleHint
            result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint);
        }
    
        return result;
    }
    

    이 방법은performSaveInstanceState 방법을 통해 Fragment의 onSaveInstanceState 리셋 방법을 터치하고 Fragment 하위 클래스는 이 방법을 다시 써서 데이터를 저장할 수 있습니다.이 Fragment에view가 설정되어 있으면 Fragment의 mInnerView 구성원의saveHierarchyState 방법으로 보기 트리를 저장합니다.
    이 일련의 저장을 진행한 후에 데이터는 모두 통합되어 Bundle에 추가되고 Activity Client Record의state 구성원이 저장하며 Fragment Manager Impl의 mState Saved는true로 표시됩니다.
    상태 복구
    FragmentActivity로 들어가는 onCreate 방법: [FragmentActivity.java]
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);
    
        if (savedInstanceState != null) {
            //  savedInstanceState    ,  FRAGMENTS_TAG     
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            //      FragmentManagerImpl      
            mFragments.restoreSaveState(p);
    
            // Check if there are any pending onActivityResult calls to descendent Fragments.
            if (savedInstanceState.containsKey(NEXT_CANDIDATE_REQUEST_INDEX_TAG)) {
                mNextCandidateRequestIndex =
                        savedInstanceState.getInt(NEXT_CANDIDATE_REQUEST_INDEX_TAG);
                int[] requestCodes = savedInstanceState.getIntArray(ALLOCATED_REQUEST_INDICIES_TAG);
                String[] fragmentWhos = savedInstanceState.getStringArray(REQUEST_FRAGMENT_WHO_TAG);
                if (requestCodes == null || fragmentWhos == null ||
                            requestCodes.length != fragmentWhos.length) {
                    Log.w(TAG, "Invalid requestCode mapping in savedInstanceState.");
                } else {
                    mPendingFragmentActivityResults = new SparseArrayCompat<>(requestCodes.length);
                    for (int i = 0; i < requestCodes.length; i++) {
                        mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]);
                    }
                }
            }
        }
    
        if (mPendingFragmentActivityResults == null) {
            mPendingFragmentActivityResults = new SparseArrayCompat<>();
            mNextCandidateRequestIndex = 0;
        }
    
        super.onCreate(savedInstanceState);
    
        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
        mFragments.dispatchCreate();
    }
    

    onCreate에서 savedInstanceState가 비어 있지 않으면 FRAGMENTS 를 꺼냅니다TAG에 대응하는 데이터는 FragmentManagerImpl의restoreSaveState 방법으로 상태를 회복합니다.
    이어서 restore Save State 방법을 보십시오: [Fragment Manager Impl.java]
    void restoreSaveState(Parcelable state) {
        // If there is no saved state at all, then there's nothing else to do
        if (state == null) return;
        FragmentManagerState fms = (FragmentManagerState)state;
        if (fms.mActive == null) return;
    
        // First re-attach any non-config instances we are retaining back
        // to their saved state, so we don't try to instantiate them again.
        //   setRetainInstance   Fragment     
        for (Fragment f : mNonConfig.getRetainedFragments()) {
            if (DEBUG) Log.v(TAG, "restoreSaveState: re-attaching retained " + f);
            FragmentState fs = null;
            for (FragmentState fragmentState : fms.mActive) {
                if (fragmentState.mWho.equals(f.mWho)) {
                    fs = fragmentState;
                    break;
                }
            }
            if (fs == null) {
                //  RetainedFragment   RetainedFragment   ,      RetainedFragment
                if (DEBUG) {
                    Log.v(TAG, "Discarding retained Fragment " + f
                            + " that was not found in the set of active Fragments " + fms.mActive);
                }
                // We need to ensure that onDestroy and any other clean up is done
                // so move the Fragment up to CREATED, then mark it as being removed, then
                // destroy it.
                moveToState(f, Fragment.CREATED, 0, 0, false);
                f.mRemoving = true;
                moveToState(f, Fragment.INITIALIZING, 0, 0, false);
                continue;
            }
            fs.mInstance = f;
            f.mSavedViewState = null;
            f.mBackStackNesting = 0;
            f.mInLayout = false;
            f.mAdded = false;
            f.mTargetWho = f.mTarget != null ? f.mTarget.mWho : null;
            f.mTarget = null;
            if (fs.mSavedFragmentState != null) {
                //   RetainedFragment   
            fs.mSavedFragmentState.setClassLoader(mHost.getContext().getClassLoader());
                f.mSavedViewState = fs.mSavedFragmentState.getSparseParcelableArray(
                        FragmentManagerImpl.VIEW_STATE_TAG);
                f.mSavedFragmentState = fs.mSavedFragmentState;
            }
        }
    
        // Build the full list of active fragments, instantiating them from
        // their saved state.
        mActive.clear();
        for (FragmentState fs : fms.mActive) {
            if (fs != null) {
                //    Fragment,  FragmentState         Fragment
                Fragment f = fs.instantiate(mHost.getContext().getClassLoader(),
                        getFragmentFactory());
                f.mFragmentManager = this;
                if (DEBUG) Log.v(TAG, "restoreSaveState: active (" + f.mWho + "): " + f);
                //   fragment mActive   
                mActive.put(f.mWho, f);
                // Now that the fragment is instantiated (or came from being
                // retained above), clear mInstance in case we end up re-restoring
                // from this FragmentState again.
                fs.mInstance = null;
            }
        }
    
        // Build the list of currently added fragments.
        mAdded.clear();
        if (fms.mAdded != null) {
            for (String who : fms.mAdded) {
                //             fragment
                Fragment f = mActive.get(who);
                if (f == null) {
                    throwException(new IllegalStateException(
                            "No instantiated fragment for (" + who + ")"));
                }
                f.mAdded = true;
                if (DEBUG) Log.v(TAG, "restoreSaveState: added (" + who + "): " + f);
                if (mAdded.contains(f)) {
                    throw new IllegalStateException("Already added " + f);
                }
                synchronized (mAdded) {
                    //    mAdded   
                    mAdded.add(f);
                }
            }
        }
    
        // Build the back stack.
        //        
        if (fms.mBackStack != null) {
            mBackStack = new ArrayList<BackStackRecord>(fms.mBackStack.length);
            for (int i=0; i<fms.mBackStack.length; i++) {
                BackStackRecord bse = fms.mBackStack[i].instantiate(this);
                if (DEBUG) {
                    Log.v(TAG, "restoreAllState: back stack #" + i
                            + " (index " + bse.mIndex + "): " + bse);
                    LogWriter logw = new LogWriter(TAG);
                    PrintWriter pw = new PrintWriter(logw);
                    bse.dump("  ", pw, false);
                    pw.close();
                }
                mBackStack.add(bse);
                if (bse.mIndex >= 0) {
                    setBackStackIndex(bse.mIndex, bse);
                }
            }
        } else {
            mBackStack = null;
        }
    
        if (fms.mPrimaryNavActiveWho != null) {
            mPrimaryNav = mActive.get(fms.mPrimaryNavActiveWho);
            dispatchParentPrimaryNavigationFragmentChanged(mPrimaryNav);
        }
        this.mNextFragmentIndex = fms.mNextFragmentIndex;
    }
    

    이 방법에서는 Fragment Manager State에 저장된 데이터를 이용하여 상태 회복을 하는데, 그 중에서 Fragment State를 이용하여 Fragment 데이터를 복원할 때 mSaved Fragment State에 값을 부여하여 Fragment의 mSaved Fragment State 구성원에게 준다.Fragment의 사용자 정의 저장 데이터와 보기 트리 상태가 mSavedFragmentState에 저장됨
    이후 FragmentActivity 스케줄링Fragment 표시에 따라 Fragment의 라이프 사이클별 단계에서 mSavedFragmentState로 저장된 데이터 복구를 사용할 수 있습니다.
    Fragment Manager Impl에 대한 라이프 사이클 상태 스케줄링 방법 moveToState: [Fragment Manager Impl.java]
    void moveToState(Fragment f, int newState, int transit, int transitionStyle,
                     boolean keepActive) {
        // ···
        if (f.mState <= newState) {
            // ···
            switch (f.mState) {
                case Fragment.INITIALIZING:
                    if (newState > Fragment.INITIALIZING) {
                        if (DEBUG) Log.v(TAG, "moveto CREATED: " + f);
                        if (f.mSavedFragmentState != null) {
                            f.mSavedFragmentState.setClassLoader(mHost.getContext()
                                    .getClassLoader());
                            //           
                            f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray(
                                    FragmentManagerImpl.VIEW_STATE_TAG);
                            Fragment target = getFragment(f.mSavedFragmentState,
                                    FragmentManagerImpl.TARGET_STATE_TAG);
                            f.mTargetWho = target != null ? target.mWho : null;
                            if (f.mTargetWho != null) {
                                f.mTargetRequestCode = f.mSavedFragmentState.getInt(
                                        FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0);
                            }
                            if (f.mSavedUserVisibleHint != null) {
                                f.mUserVisibleHint = f.mSavedUserVisibleHint;
                                f.mSavedUserVisibleHint = null;
                            } else {
                                //          
                                f.mUserVisibleHint = f.mSavedFragmentState.getBoolean(
                                        FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true);
                            }
                            if (!f.mUserVisibleHint) {
                                f.mDeferStart = true;
                                if (newState > Fragment.ACTIVITY_CREATED) {
                                    newState = Fragment.ACTIVITY_CREATED;
                                }
                            }
                        }
    
                        // ···
                        
                        if (f.mParentFragment == null) {
                            mHost.onAttachFragment(f);
                        } else {
                            f.mParentFragment.onAttachFragment(f);
                        }
                        dispatchOnFragmentAttached(f, mHost.getContext(), false);
    
                        if (!f.mIsCreated) {
                            dispatchOnFragmentPreCreated(f, f.mSavedFragmentState, false);
                            //   Fragment onCreate    ,   bundle
                            f.performCreate(f.mSavedFragmentState);
                            dispatchOnFragmentCreated(f, f.mSavedFragmentState, false);
                        } else {
                            //   Fragment  Fragment     
                            f.restoreChildFragmentState(f.mSavedFragmentState);
                            f.mState = Fragment.CREATED;
                        }
                    }
                    // fall through
                case Fragment.CREATED:
                    // We want to unconditionally run this anytime we do a moveToState that
                    // moves the Fragment above INITIALIZING, including cases such as when
                    // we move from CREATED => CREATED as part of the case fall through above.
                    if (newState > Fragment.INITIALIZING) {
                        ensureInflatedFragmentView(f);
                    }
    
                    if (newState > Fragment.CREATED) {
                        if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f);
                        if (!f.mFromLayout) {
                            ViewGroup container = null;
                            if (f.mContainerId != 0) {
                                // ···
                            }
                            f.mContainer = container;
                            //   Fragment onCreateView    ,   bundle
                            f.performCreateView(f.performGetLayoutInflater(
                                    f.mSavedFragmentState), container, f.mSavedFragmentState);
                            if (f.mView != null) {
                                f.mInnerView = f.mView;
                                f.mView.setSaveFromParentEnabled(false);
                                if (container != null) {
                                    container.addView(f.mView);
                                }
                                if (f.mHidden) {
                                    f.mView.setVisibility(View.GONE);
                                }
                                //   Fragment onViewCreated    ,   bundle
                                f.onViewCreated(f.mView, f.mSavedFragmentState);
                                dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
                                        false);
                                // Only animate the view if it is visible. This is done after
                                // dispatchOnFragmentViewCreated in case visibility is changed
                                f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
                                        && f.mContainer != null;
                            } else {
                                f.mInnerView = null;
                            }
                        }
    
                        //   Fragment onActivityCreated    ,   bundle
                        f.performActivityCreated(f.mSavedFragmentState);
                        dispatchOnFragmentActivityCreated(f, f.mSavedFragmentState, false);
                        if (f.mView != null) {
                            //   mInnerView restoreHierarchyState    mSavedViewState         。
                            //     onViewStateRestored     bundle。
                            f.restoreViewState(f.mSavedFragmentState);
                        }
                        // mSavedFragmentState    
                        f.mSavedFragmentState = null;
                    }
                    // fall through
                case Fragment.ACTIVITY_CREATED:
                    if (newState > Fragment.ACTIVITY_CREATED) {
                        if (DEBUG) Log.v(TAG, "moveto STARTED: " + f);
                        f.performStart();
                        dispatchOnFragmentStarted(f, false);
                    }
                    // fall through
                case Fragment.STARTED:
                    if (newState > Fragment.STARTED) {
                        if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f);
                        f.performResume();
                        dispatchOnFragmentResumed(f, false);
                        f.mSavedFragmentState = null;
                        f.mSavedViewState = null;
                    }
            }
        } else if (f.mState > newState) { /* ··· */ }
        // ···
    }
    

    이 방법에서 보듯이 Fragment의 생명주기 리셋 방법에서 받은 saved InstanceState 인삼은 바로 Fragment의 mSaved Fragment State이고 발송이 완료된 후에 mSaved Fragment State에 값을 비운다.
    commit & commitAllowingStateLoss
    여기에서 제출 업무의 두 가지 방법을 보십시오:commit,commit Allowing State Loss. 말하자면, 하나는 제출할 때 상태를 잃어버리는 것을 허용하지 않고, 다른 하나는 허용합니다.
    [BackStackRecord.java]
    public int commit() {
        return commitInternal(false);
    }
    
    public int commitAllowingStateLoss() {
        return commitInternal(true);
    }
    

    모두 같은 방법의commitInternal을 호출했습니다. 다만 전송 매개 변수가 다르기 때문에 하나는false이고 다른 하나는true입니다.
    commitInternal 메서드 보기: [BackStackRecord.java]
    int commitInternal(boolean allowStateLoss) {
        // ···
        mManager.enqueueAction(this, allowStateLoss);
        return mIndex;
    }
    

    allowStateLoss 매개 변수는 트랜잭션 대기열을 추가하는 방법으로 직접 전달됩니다.
    [FragmentManagerImpl.java]
    public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
        if (!allowStateLoss) {
            //         ,      
            checkStateLoss();
        }
        synchronized (this) {
            if (mDestroyed || mHost == null) {
                //  FragmentActivity  destroyed   FragmentHostCallback   ,        ,     。
                if (allowStateLoss) {
                    // This FragmentManager isn't attached, so drop the entire transaction.
                    return;
                }
                throw new IllegalStateException("Activity has been destroyed");
            }
            if (mPendingActions == null) {
                mPendingActions = new ArrayList<>();
            }
            mPendingActions.add(action);
            scheduleCommit();
        }
    }
    

    상태 분실을 허용할지 여부는 업무를 대기열에 추가하기 전에 상태를 확인하고 이상을 던지는지 확인하는 것입니다.
    checkStateLoss에 들어가는 방법: [Fragment ManagerImpl.java]
    private void checkStateLoss() {
        //     ,       
        if (isStateSaved()) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
    }
    
    @Override
    public boolean isStateSaved() {
        // See saveAllState() for the explanation of this.  We do this for
        // all platform versions, to keep our behavior more consistent between
        // them.
        // onSaveInstanceState mStateSaved   true,onStop mStopped   true
        return mStateSaved || mStopped;
    }
    

    Fragment 트랜잭션 작업은 onSaveInstanceState 콜백 방법을 수행한 후 추가할 수 없습니다.Fragment의 상태 저장은 onSaveInstanceState 단계에서, 이후에 Fragment Manager Impl의 Fragment를 변경하면 이러한 Fragment의 상태가 저장되지 않기 때문이다.
    onSaveInstanceState 리셋은 안드로이드 P 및 이상 버전의 리셋 시기가 onStop 이후이고 낮은 버전의 리셋 시기가 onStop 이전입니다.
    총결산
    FragmentActivity는 onSaveInstanceState 콜백 방법에서 FragmentManagerImpl을 호출하여 관리하는 Fragment에 상태 저장을 알립니다.저장하는 동안 Fragment의 onSaveInstanceState 콜백을 트리거하여 사용자 정의 데이터를 다시 작성할 수 있습니다.Fragment에 설정view가 있으면 보기 트리도 저장됩니다.
    FragmentActivity는onCreate 리셋 방법에서savedInstanceState 매개 변수에 값이 있는지 판단한 다음FragmentManagerImpl에 상태 회복을 알리고 Fragment를 실례화하여 상태를 저장하는 bundle를 mSavedFragmentState 구성원에게 부여한다.
    Fragment의 생성에서 표시까지의 라이프 사이클 상태 성장 단계에서 mSavedFragmentState 구성원을 통해 데이터와 보기 트리를 복원하고 mSavedFragmentState를 인삼 호출에 대응하는 라이프 사이클 리셋 방법으로 사용하며 완성된 후에 mSavedFragmentState를 지웁니다.Fragment 하위 클래스는 onCreate 및 onActivityCreated에서 사용자 정의 데이터를 복원합니다.

    좋은 웹페이지 즐겨찾기