android 입력법의 국제화 실현
먼저 우리가 모든Activity 전시의 국제화를 어떻게 실현하는지 살펴보자. 정상적으로 우리는 원생태의setting에서 실현하지 못한다. 대부분이 현재 맞춤형이기 때문이다. 우리도 마찬가지다. 다음은 우리가 설정한 응용 언어의 전환 실현 기능 코드이다.
try {
Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");
Object am = activityManagerNative.getMethod("getDefault").invoke(activityManagerNative);
Object config = am.getClass().getMethod("getConfiguration").invoke(am);
config.getClass().getDeclaredField("locale").set(config, language);
config.getClass().getDeclaredField("userSetLocale").setBoolean(config, true);
am.getClass().getMethod("updateConfiguration", android.content.res.Configuration.class).invoke(am, config);
} catch (Exception e) {
}
위에서 보듯이 우리는 반사 방법을 통해 다음과 같은 방법을 호출했다.
IActivityManager am = ActivityManagerNative.getDefault();
Configuration config = am.getConfiguration();
config.locale = locale;
config.userSetLocale = true;
am.updateConfiguration(config);
저희가 먼저 얻었어요.
Activity ManagerNative의 getDefault () 대상입니다. 이 코드를 추적하십시오.
<span style="font-family: Arial; padding: 0px; color: rgb(51, 51, 51);"> /**
* Retrieve the system‘s default/global activity manager.
*/
static public IActivityManager getDefault() {
return gDefault.get();
}</span>
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
그럼 서비스 관리자.getService("activity");어떤 대상을 얻었을까요?
Activity Manager Service에는 다음과 같은 정의가 있습니다.
public static void setSystemProcess() {
try {
ActivityManagerService m = mSelf;
ServiceManager.addService("activity", m);
위에서 보듯이 하나는dd서비스로 가고 하나는get으로 간다. get의 대상은Activity Manager 서비스이다. 물론 여기에서 나는 직접 찾았다. 사실 이Activity Manager 서비스는 시작부터 프로세스 이름 변경에 이르기까지 절차가 있다. 그래, Activity Manager Service의 업데이트 Configuration(Configuration values) 방법을 보자.
public void updateConfiguration(Configuration values) {
enforceCallingPermission(android.Manifest.permission.CHANGE_CONFIGURATION,
"updateConfiguration()");
synchronized(this) {
if (values == null && mWindowManager != null) {
// sentinel: fetch the current configuration from the window manager
values = mWindowManager.computeNewConfiguration();
}
if (mWindowManager != null) {
mProcessList.applyDisplaySize(mWindowManager);
}
final long origId = Binder.clearCallingIdentity();
if (values != null) {
Settings.System.clearConfiguration(values);
}
updateConfigurationLocked(values, null, false, false);
Binder.restoreCallingIdentity(origId);
}
}
위의 enforceCallingPermission 방법은 권한 검증을 하고 업데이트ConfigurationLocked(values,null,false,false)를 중점적으로 본다.
/**
* Do either or both things: (1) change the current configuration, and (2)
* make sure the given activity is running with the (now) current
* configuration. Returns true if the activity has been left running, or
* false if <var>starting</var> is being destroyed to match the new
* configuration.
* @param persistent TODO
*/
public boolean updateConfigurationLocked(Configuration values,
ActivityRecord starting, boolean persistent, boolean initLocale) {
int changes = 0;
boolean kept = true;
if (values != null) {
Configuration newConfig = new Configuration(mConfiguration);
changes = newConfig.updateFrom(values);
if (changes != 0) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) {
Slog.i(TAG, "Updating configuration to: " + values);
}
EventLog.writeEvent(EventLogTags.CONFIGURATION_CHANGED, changes);
if (values.locale != null && !initLocale) {
saveLocaleLocked(values.locale,
!values.locale.equals(mConfiguration.locale),
values.userSetLocale);
}
mConfigurationSeq++;
if (mConfigurationSeq <= 0) {
mConfigurationSeq = 1;
}
newConfig.seq = mConfigurationSeq;
mConfiguration = newConfig;
Slog.i(TAG, "Config changed: " + newConfig);
final Configuration configCopy = new Configuration(mConfiguration);
AttributeCache ac = AttributeCache.instance();
if (ac != null) {
ac.updateConfiguration(configCopy);
}
// Make sure all resources in our process are updated
// right now, so that anyone who is going to retrieve
// resource values after we return will be sure to get
// the new ones. This is especially important during
// boot, where the first config change needs to guarantee
// all resources have that config before following boot
// code is executed.
mSystemThread.applyConfigurationToResources(configCopy);
if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {
Message msg = mHandler.obtainMessage(UPDATE_CONFIGURATION_MSG);
msg.obj = new Configuration(configCopy);
mHandler.sendMessage(msg);
}
for (int i=mLruProcesses.size()-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
try {
if (app.thread != null) {
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Sending to proc "
+ app.processName + " new config " + mConfiguration);
app.thread.scheduleConfigurationChanged(configCopy);
}
} catch (Exception e) {
}
}
Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
| Intent.FLAG_RECEIVER_REPLACE_PENDING);
broadcastIntentLocked(null, null, intent, null, null, 0, null, null,
null, false, false, MY_PID, Process.SYSTEM_UID);
if ((changes&ActivityInfo.CONFIG_LOCALE) != 0) {
broadcastIntentLocked(null, null,
new Intent(Intent.ACTION_LOCALE_CHANGED),
null, null, 0, null, null,
null, false, false, MY_PID, Process.SYSTEM_UID);
}
}
}
if (changes != 0 && starting == null) {
// If the configuration changed, and the caller is not already
// in the process of starting an activity, then find the top
// activity to check if its configuration needs to change.
starting = mMainStack.topRunningActivityLocked(null);
}
if (starting != null) {
kept = mMainStack.ensureActivityConfigurationLocked(starting, changes);
// And we need to make sure at this point that all other activities
// are made visible with the correct configuration.
mMainStack.ensureActivitiesVisibleLocked(starting, changes);
}
if (values != null && mWindowManager != null) {
mWindowManager.setNewConfiguration(mConfiguration);
}
return kept;
}
위의 이 방법은 약간의 코드 줄이 비교적 많은데, 우리가 먼저 본 주석을 번역해 보자. 허허, 아래와 같다
(1) 현재 설정을 변경하는 것은 통속적으로 말하면 변경된 configuration을 현재 configuration으로 업데이트하는 것이다
(2) 실행 중인 모든 activity가 변경된 configuration을 업데이트할 수 있도록 확보
주석은 여전히 비교적 명확하니, 우리는 아래의 이 방법을 중점적으로 본다
for (int i=mLruProcesses.size()-1; i>=0; i--) {
ProcessRecord app = mLruProcesses.get(i);
try {
if (app.thread != null) {
if (DEBUG_CONFIGURATION) Slog.v("PateoConfig", "Sending to proc "
+ app.processName + " new config " + mConfiguration);
app.thread.scheduleConfigurationChanged(configCopy);
}
} catch (Exception e) {
}
}
/**
* List of running applications, sorted by recent usage.
* The first entry in the list is the least recently used.
* It contains ApplicationRecord objects. This list does NOT include
* any persistent application records (since we never want to exit them).
*/
final ArrayList<ProcessRecord> mLruProcesses
= new ArrayList<ProcessRecord>();
위의 설명을 보면 mLruProcesses
실행 중인 모든 프로세스를 저장합니다.apk 파일이 실행될 때 프로세스에 대응합니다.app.thread. 여기의thread가 대표하는 것은 무엇입니까?
IApplicationThread thread;
public abstract class ApplicationThreadNative extends Binder
implements IApplicationThread {
위의 실현을 보면 ApplicationThreadNative입니다. 알겠습니다. 이 ApplicationThreadNative에 들어가겠습니다.
scheduleConfigurationChanged 방법:
public final void scheduleConfigurationChanged(Configuration config)
throws RemoteException {
Parcel data = Parcel.obtain();
data.writeInterfaceToken(IApplicationThread.descriptor);
config.writeToParcel(data, 0);
mRemote.transact(SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION, data, null,
IBinder.FLAG_ONEWAY);
data.recycle();
}
이 msg 메시지 처리 좀 보세요.
case SCHEDULE_CONFIGURATION_CHANGED_TRANSACTION:
{
data.enforceInterface(IApplicationThread.descriptor);
Configuration config = Configuration.CREATOR.createFromParcel(data);
scheduleConfigurationChanged(config);
return true;
}
void scheduleConfigurationChanged(Configuration config) throws RemoteException;
구현을 찾습니다.
private class ApplicationThread extends ApplicationThreadNative {
......
public void scheduleConfigurationChanged(Configuration config) {
updatePendingConfiguration(config);
queueOrSendMessage(H.CONFIGURATION_CHANGED, config);
}
......
}
CONFIGURATION을 보도록 하겠습니다.CHANGED 메시지 처리 방법
case CONFIGURATION_CHANGED:
handleConfigurationChanged((Configuration)msg.obj, null);
break;
final void handleConfigurationChanged(Configuration config, CompatibilityInfo compat) {
ArrayList<ComponentCallbacks2> callbacks = null;
synchronized (mPackages) {
if (mPendingConfiguration != null) {
if (!mPendingConfiguration.isOtherSeqNewer(config)) {
config = mPendingConfiguration;
}
mPendingConfiguration = null;
}
if (config == null) {
return;
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handle configuration changed: "
+ config);
applyConfigurationToResourcesLocked(config, compat);
if (mConfiguration == null) {
mConfiguration = new Configuration();
}
if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
return;
}
mConfiguration.updateFrom(config);
config = applyCompatConfiguration();
callbacks = collectComponentCallbacksLocked(false, config);
}
// Cleanup hardware accelerated stuff
WindowManagerImpl.getDefault().trimLocalMemory();
if (callbacks != null) {
final int N = callbacks.size();
for (int i=0; i<N; i++) {
performConfigurationChanged(callbacks.get(i), config);
}
}
}
먼저 applyConfigurationToResourcesLocked(config,compat)를 살펴보겠습니다.
final boolean applyConfigurationToResourcesLocked(Configuration config,
CompatibilityInfo compat) {
if (DEBUG_CONFIGURATION) Slog.v("PateoConfig","ActivityThread class ,applyConfigurationToResourcesLocked coming");
if (mResConfiguration == null) {
mResConfiguration = new Configuration();
}
if (!mResConfiguration.isOtherSeqNewer(config) && compat == null) {
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Skipping new config: curSeq="
+ mResConfiguration.seq + ", newSeq=" + config.seq);
return false;
}
int changes = mResConfiguration.updateFrom(config);
DisplayMetrics dm = getDisplayMetricsLocked(null, true);
if (compat != null && (mResCompatibilityInfo == null ||
!mResCompatibilityInfo.equals(compat))) {
mResCompatibilityInfo = compat;
changes |= ActivityInfo.CONFIG_SCREEN_LAYOUT
| ActivityInfo.CONFIG_SCREEN_SIZE
| ActivityInfo.CONFIG_SMALLEST_SCREEN_SIZE;
}
// set it for java, this also affects newly created Resources
if (config.locale != null) {
Locale.setDefault(config.locale);
}
Resources.updateSystemConfiguration(config, dm, compat);
ApplicationPackageManager.configurationChanged();
//Slog.i(TAG, "Configuration changed in " + currentPackageName());
Iterator<WeakReference<Resources>> it =
mActiveResources.values().iterator();
//Iterator<Map.Entry<String, WeakReference<Resources>>> it =
// mActiveResources.entrySet().iterator();
while (it.hasNext()) {
WeakReference<Resources> v = it.next();
Resources r = v.get();
if (r != null) {
if (DEBUG_CONFIGURATION) Slog.v("PateoConfig", "ActivityThread class ,Changing resources "
+ r + " config to: " + config);
r.updateConfiguration(config, dm, compat);
//Slog.i(TAG, "Updated app resources " + v.getKey()
// + " " + r + ": " + r.getConfiguration());
} else {
//Slog.i(TAG, "Removing old resources " + v.getKey());
it.remove();
}
}
return changes != 0;
}
Resources.업데이트 시스템 Configuration () 은 시스템 자원의 일부를 지우고 config를 Resources로 업데이트합니다. Resources는 AssetManager 대상을 포함하고 이 대상의 핵심은 AssetManager입니다.cpp에서 완성된.그런 다음 mActivityResources 리소스를 순환 비웁니다.handleConfigurationChanged () 함수로 돌아가서 다음 방법을 실행하십시오
callbacks = collectComponentCallbacksLocked(false, config);
ArrayList<ComponentCallbacks2> collectComponentCallbacksLocked(
boolean allActivities, Configuration newConfig) {
ArrayList<ComponentCallbacks2> callbacks
= new ArrayList<ComponentCallbacks2>();
if (mActivities.size() > 0) {
Iterator<ActivityClientRecord> it = mActivities.values().iterator();
while (it.hasNext()) {
ActivityClientRecord ar = it.next();
Activity a = ar.activity;
if (a != null) {
Configuration thisConfig = applyConfigCompatMainThread(newConfig,
ar.packageInfo.mCompatibilityInfo.getIfNeeded());
if (!ar.activity.mFinished && (allActivities ||
(a != null && !ar.paused))) {
// If the activity is currently resumed, its configuration
// needs to change right now.
callbacks.add(a);
} else if (thisConfig != null) {
// Otherwise, we will tell it about the change
// the next time it is resumed or shown. Note that
// the activity manager may, before then, decide the
// activity needs to be destroyed to handle its new
// configuration.
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Setting activity "
+ ar.activityInfo.name + " newConfig=" + thisConfig);
ar.newConfig = thisConfig;
}
}
}
}
if (mServices.size() > 0) {
Iterator<Service> it = mServices.values().iterator();
while (it.hasNext()) {
callbacks.add(it.next());
}
}
synchronized (mProviderMap) {
if (mLocalProviders.size() > 0) {
Iterator<ProviderClientRecord> it = mLocalProviders.values().iterator();
while (it.hasNext()) {
callbacks.add(it.next().mLocalProvider);
}
}
}
final int N = mAllApplications.size();
for (int i=0; i<N; i++) {
callbacks.add(mAllApplications.get(i));
}
return callbacks;
}
올라오는 걸 보면 콜백스는 Activity 등이 매개 변수로 들어오는 거예요.
if (callbacks != null) {
final int N = callbacks.size();
for (int i=0; i<N; i++) {
performConfigurationChanged(callbacks.get(i), config);
}
}
performConfigurationChanged에 들어가는 방법
private final void performConfigurationChanged(
ComponentCallbacks2 cb, Configuration config) {
// Only for Activity objects, check that they actually call up to their
// superclass implementation. ComponentCallbacks2 is an interface, so
// we check the runtime type and act accordingly.
Activity activity = (cb instanceof Activity) ? (Activity) cb : null;
if (activity != null) {
activity.mCalled = false;
}
boolean shouldChangeConfig = false;
if ((activity == null) || (activity.mCurrentConfig == null)) {
shouldChangeConfig = true;
} else {
// If the new config is the same as the config this Activity
// is already running with then don‘t bother calling
// onConfigurationChanged
int diff = activity.mCurrentConfig.diff(config);
if (diff != 0) {
// If this activity doesn‘t handle any of the config changes
// then don‘t bother calling onConfigurationChanged as we‘re
// going to destroy it.
if ((~activity.mActivityInfo.getRealConfigChanged() & diff) == 0) {
shouldChangeConfig = true;
}
}
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Config callback " + cb
+ ": shouldChangeConfig=" + shouldChangeConfig);
if (shouldChangeConfig) {
cb.onConfigurationChanged(config);
if (activity != null) {
if (!activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + activity.getLocalClassName() +
" did not call through to super.onConfigurationChanged()");
}
activity.mConfigChangeFlags = 0;
activity.mCurrentConfig = new Configuration(config);
}
}
}
위쪽
configuration이 바뀌었는지 판단합니다. 바뀌면 shouldChangeConfig는true입니다.그런 다음 activity의 onConfigurationChange(config)를 호출합니다.
/**
* Called by the system when the device configuration changes while your
* activity is running. Note that this will <em>only</em> be called if
* you have selected configurations you would like to handle with the
* {@link android.R.attr#configChanges} attribute in your manifest. If
* any configuration change occurs that is not selected to be reported
* by that attribute, then instead of reporting it the system will stop
* and restart the activity (to have it launched with the new
* configuration).
*
* <p>At the time that this function has been called, your Resources
* object will have been updated to return resource values matching the
* new configuration.
*
* @param newConfig The new device configuration.
*/
public void onConfigurationChanged(Configuration newConfig) {
mCalled = true;
mFragments.dispatchConfigurationChanged(newConfig);
if (mWindow != null) {
// Pass the configuration changed event to the window
mWindow.onConfigurationChanged(newConfig);
}
if (mActionBar != null) {
// Do this last; the action bar will need to access
// view changes from above.
mActionBar.onConfigurationChanged(newConfig);
}
}
위의 설명을 보면 다음과 같습니다.
만약 당신의activity가 실행된다면, 장치 정보가 바뀌었을 때 (즉configuration이 바뀌었을 때) 시스템에서 호출됩니다.만약 당신이 manfest에 있다면.xml에 configChnages 속성을 설정하면 configurationchange를 직접 처리할 수 있음을 표시합니다.그렇지 않으면 현재 이 activity를 다시 시작합니다.다시 시작하기 전에 낡은 Resources가 비워지면 새로운 자원을 불러옵니다. 전체 과정이 언어 전환을 완성한 후에 모든 app가 새로운 언어를 사용할 수 있습니다.
그렇다면 Activity를 다시 시작하는 코드가 어디에 있는지 물어보고 Activity Manager 서비스의 업데이트 Configuration Locked 방법으로 돌아가 앱을 분석해 봅시다.thread.scheduleConfigurationChanged(configCopy);의 코드 후, 사실 app에서.thread.scheduleConfigurationChanged(configCopy);이 코드 아래에는 업데이트 ConfigurationLocked 메소드 내에 이러한 코드가 있습니다.
if (changes != 0 && starting == null) {
// If the configuration changed, and the caller is not already
// in the process of starting an activity, then find the top
// activity to check if its configuration needs to change.
starting = mMainStack.topRunningActivityLocked(null);
}
if (starting != null) {
kept = mMainStack.ensureActivityConfigurationLocked(starting, changes);
주로
kept = mMainStack.ensureActivityConfigurationLocked(starting, changes);
우리는 이 방법에 들어간다
/**
* Make sure the given activity matches the current configuration. Returns
* false if the activity had to be destroyed. Returns true if the
* configuration is the same, or the activity will remain running as-is
* for whatever reason. Ensures the HistoryRecord is updated with the
* correct configuration and all other bookkeeping is handled.
*/
final boolean ensureActivityConfigurationLocked(ActivityRecord r,
int globalChanges) {
if (mConfigWillChange) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Skipping config check (will change): " + r);
return true;
}
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Ensuring correct configuration: " + r);
// Short circuit: if the two configurations are the exact same
// object (the common case), then there is nothing to do.
Configuration newConfig = mService.mConfiguration;
if (r.configuration == newConfig && !r.forceNewConfig) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Configuration unchanged in " + r);
return true;
}
// We don‘t worry about activities that are finishing.
if (r.finishing) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Configuration doesn‘t matter in finishing " + r);
r.stopFreezingScreenLocked(false);
return true;
}
// Okay we now are going to make this activity have the new config.
// But then we need to figure out how it needs to deal with that.
Configuration oldConfig = r.configuration;
r.configuration = newConfig;
// Determine what has changed. May be nothing, if this is a config
// that has come back from the app after going idle. In that case
// we just want to leave the official config object now in the
// activity and do nothing else.
final int changes = oldConfig.diff(newConfig);
if (changes == 0 && !r.forceNewConfig) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Configuration no differences in " + r);
return true;
}
// If the activity isn‘t currently running, just leave the new
// configuration and it will pick that up next time it starts.
if (r.app == null || r.app.thread == null) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Configuration doesn‘t matter not running " + r);
r.stopFreezingScreenLocked(false);
r.forceNewConfig = false;
return true;
}
// Figure out how to handle the changes between the configurations.
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) {
Slog.v(TAG, "Checking to restart " + r.info.name + ": changed=0x"
+ Integer.toHexString(changes) + ", handles=0x"
+ Integer.toHexString(r.info.getRealConfigChanged())
+ ", newConfig=" + newConfig);
}
if ((changes&(~r.info.getRealConfigChanged())) != 0 || r.forceNewConfig) {
// Aha, the activity isn‘t handling the change, so DIE DIE DIE.
r.configChangeFlags |= changes;
r.startFreezingScreenLocked(r.app, globalChanges);
r.forceNewConfig = false;
if (r.app == null || r.app.thread == null) {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Switch is destroying non-running " + r);
destroyActivityLocked(r, true, false, "config");
} else if (r.state == ActivityState.PAUSING) {
// A little annoying: we are waiting for this activity to
// finish pausing. Let‘s not do anything now, but just
// flag that it needs to be restarted when done pausing.
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Switch is skipping already pausing " + r);
r.configDestroy = true;
return true;
} else if (r.state == ActivityState.RESUMED) {
// Try to optimize this case: the configuration is changing
// and we need to restart the top, resumed activity.
// Instead of doing the normal handshaking, just say
// "restart!".
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Switch is restarting resumed " + r);
relaunchActivityLocked(r, r.configChangeFlags, true);
r.configChangeFlags = 0;
} else {
if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.v(TAG,
"Switch is restarting non-resumed " + r);
relaunchActivityLocked(r, r.configChangeFlags, false);
r.configChangeFlags = 0;
}
// All done... tell the caller we weren‘t able to keep this
// activity around.
return false;
}
// Default case: the activity can handle this new configuration, so
// hand it over. Note that we don‘t need to give it the new
// configuration, since we always send configuration changes to all
// process when they happen so it can just use whatever configuration
// it last got.
if (r.app != null && r.app.thread != null) {
try {
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Sending new config to " + r);
r.app.thread.scheduleActivityConfigurationChanged(r.appToken);
} catch (RemoteException e) {
// If process died, whatever.
}
}
r.stopFreezingScreenLocked(false);
return true;
}
위에 코드에 로그 출력이 있는 걸 발견할 수 있을 거예요.
V/ActivityManager( 1265): Switch is restarting resumed ActivityRecord{41ad1e90 com.pateo.as.settings/.activity.SettingActivity}
V/ActivityManager( 1265): Relaunching: ActivityRecord{41ad1e90 com.pateo.as.settings/.activity.SettingActivity} with results=null newIntents=null andResume=true
I/ActivityManager( 1265): Switch is restarting resumed ActivityRecord{41ad1e90 com.pateo.as.settings/.activity.SettingActivity}
출력 로그를 보면 다음과 같은 방법으로 들어갑니다.
relaunchActivityLocked(r, r.configChangeFlags, true);
private final boolean relaunchActivityLocked(ActivityRecord r,
int changes, boolean andResume) {
List<ResultInfo> results = null;
List<Intent> newIntents = null;
if (andResume) {
results = r.results;
newIntents = r.newIntents;
}
if (DEBUG_SWITCH) Slog.v(TAG, "Relaunching: " + r
+ " with results=" + results + " newIntents=" + newIntents
+ " andResume=" + andResume);
EventLog.writeEvent(andResume ? EventLogTags.AM_RELAUNCH_RESUME_ACTIVITY
: EventLogTags.AM_RELAUNCH_ACTIVITY, System.identityHashCode(r),
r.task.taskId, r.shortComponentName);
r.startFreezingScreenLocked(r.app, 0);
try {
if (DEBUG_SWITCH) Slog.i(TAG, "Switch is restarting resumed " + r);
r.forceNewConfig = false;
r.app.thread.scheduleRelaunchActivity(r.appToken, results, newIntents,
changes, !andResume, new Configuration(mService.mConfiguration));
// Note: don‘t need to call pauseIfSleepingLocked() here, because
// the caller will only pass in ‘andResume‘ if this activity is
// currently resumed, which implies we aren‘t sleeping.
} catch (RemoteException e) {
return false;
}
if (andResume) {
r.results = null;
r.newIntents = null;
if (mMainStack) {
mService.reportResumedActivityLocked(r);
}
}
return true;
}
위의 주요 호출은
r.app.thread.scheduleRelaunchActivity(r.appToken, results, newIntents,
changes, !andResume, new Configuration(mService.mConfiguration));
이 schedule Relaunch Activity 방법을 보십시오.public interface Iapplication Thread 클래스에 있습니다. 사실은 지금Activity Thread에 있습니다.
public final void scheduleRelaunchActivity(IBinder token,
List<ResultInfo> pendingResults, List<Intent> pendingNewIntents,
int configChanges, boolean notResumed, Configuration config) {
requestRelaunchActivity(token, pendingResults, pendingNewIntents,
configChanges, notResumed, config, true);
}
public final void requestRelaunchActivity(IBinder token,
List<ResultInfo> pendingResults, List<Intent> pendingNewIntents,
int configChanges, boolean notResumed, Configuration config,
boolean fromServer) {
ActivityClientRecord target = null;
synchronized (mPackages) {
for (int i=0; i<mRelaunchingActivities.size(); i++) {
ActivityClientRecord r = mRelaunchingActivities.get(i);
if (r.token == token) {
target = r;
if (pendingResults != null) {
if (r.pendingResults != null) {
r.pendingResults.addAll(pendingResults);
} else {
r.pendingResults = pendingResults;
}
}
if (pendingNewIntents != null) {
if (r.pendingIntents != null) {
r.pendingIntents.addAll(pendingNewIntents);
} else {
r.pendingIntents = pendingNewIntents;
}
}
break;
}
}
if (target == null) {
target = new ActivityClientRecord();
target.token = token;
target.pendingResults = pendingResults;
target.pendingIntents = pendingNewIntents;
if (!fromServer) {
ActivityClientRecord existing = mActivities.get(token);
if (existing != null) {
target.startsNotResumed = existing.paused;
}
target.onlyLocalRequest = true;
}
mRelaunchingActivities.add(target);
queueOrSendMessage(H.RELAUNCH_ACTIVITY, target);
}
if (fromServer) {
target.startsNotResumed = notResumed;
target.onlyLocalRequest = false;
}
if (config != null) {
target.createdConfig = config;
}
target.pendingConfigChanges |= configChanges;
}
}
이어서 소식의 처리를 살펴보자
case RELAUNCH_ACTIVITY: {
ActivityClientRecord r = (ActivityClientRecord)msg.obj;
handleRelaunchActivity(r);
} break;
private void handleRelaunchActivity(ActivityClientRecord tmp) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
Configuration changedConfig = null;
int configChanges = 0;
// First: make sure we have the most recent configuration and most
// recent version of the activity, or skip it if some previous call
// had taken a more recent version.
synchronized (mPackages) {
int N = mRelaunchingActivities.size();
IBinder token = tmp.token;
tmp = null;
for (int i=0; i<N; i++) {
ActivityClientRecord r = mRelaunchingActivities.get(i);
if (r.token == token) {
tmp = r;
configChanges |= tmp.pendingConfigChanges;
mRelaunchingActivities.remove(i);
i--;
N--;
}
}
if (tmp == null) {
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Abort, activity not relaunching!");
return;
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Relaunching activity "
+ tmp.token + " with configChanges=0x"
+ Integer.toHexString(configChanges));
if (mPendingConfiguration != null) {
changedConfig = mPendingConfiguration;
mPendingConfiguration = null;
}
}
if (tmp.createdConfig != null) {
// If the activity manager is passing us its current config,
// assume that is really what we want regardless of what we
// may have pending.
if (mConfiguration == null
|| (tmp.createdConfig.isOtherSeqNewer(mConfiguration)
&& mConfiguration.diff(tmp.createdConfig) != 0)) {
if (changedConfig == null
|| tmp.createdConfig.isOtherSeqNewer(changedConfig)) {
changedConfig = tmp.createdConfig;
}
}
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Relaunching activity "
+ tmp.token + ": changedConfig=" + changedConfig);
// If there was a pending configuration change, execute it first.
if (changedConfig != null) {
handleConfigurationChanged(changedConfig, null);
}
ActivityClientRecord r = mActivities.get(tmp.token);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Handling relaunch of " + r);
if (r == null) {
return;
}
r.activity.mConfigChangeFlags |= configChanges;
r.onlyLocalRequest = tmp.onlyLocalRequest;
Intent currentIntent = r.activity.mIntent;
r.activity.mChangingConfigurations = true;
// Need to ensure state is saved.
if (!r.paused) {
performPauseActivity(r.token, false, r.isPreHoneycomb());
}
if (r.state == null && !r.stopped && !r.isPreHoneycomb()) {
r.state = new Bundle();
r.state.setAllowFds(false);
mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state);
}
handleDestroyActivity(r.token, false, configChanges, true);
r.activity = null;
r.window = null;
r.hideForNow = false;
r.nextIdle = null;
// Merge any pending results and pending intents; don‘t just replace them
if (tmp.pendingResults != null) {
if (r.pendingResults == null) {
r.pendingResults = tmp.pendingResults;
} else {
r.pendingResults.addAll(tmp.pendingResults);
}
}
if (tmp.pendingIntents != null) {
if (r.pendingIntents == null) {
r.pendingIntents = tmp.pendingIntents;
} else {
r.pendingIntents.addAll(tmp.pendingIntents);
}
}
r.startsNotResumed = tmp.startsNotResumed;
handleLaunchActivity(r, currentIntent);
}
위에서 우리는 빗질하여 다음과 같은 중요한 세 가지 방법을 사용하였다
1、performPauseActivity(r.token, false, r.isPreHoneycomb());
2、handleDestroyActivity(r.token, false, configChanges, true);
3、handleLaunchActivity(r, currentIntent);
첫 번째 방법인 performPauseActivity를 보도록 하겠습니다.
final Bundle performPauseActivity(IBinder token, boolean finished,
boolean saveState) {
ActivityClientRecord r = mActivities.get(token);
return r != null ? performPauseActivity(r, finished, saveState) : null;
}
final Bundle performPauseActivity(ActivityClientRecord r, boolean finished,
boolean saveState) {
if (r.paused) {
if (r.activity.mFinished) {
// If we are finishing, we won‘t call onResume() in certain cases.
// So here we likewise don‘t want to call onPause() if the activity
// isn‘t resumed.
return null;
}
RuntimeException e = new RuntimeException(
"Performing pause of activity that is not resumed: "
+ r.intent.getComponent().toShortString());
Slog.e(TAG, e.getMessage(), e);
}
Bundle state = null;
if (finished) {
r.activity.mFinished = true;
}
try {
// Next have the activity save its current state and managed dialogs...
if (!r.activity.mFinished && saveState) {
state = new Bundle();
state.setAllowFds(false);
mInstrumentation.callActivityOnSaveInstanceState(r.activity, state);
r.state = state;
}
// Now we are idle.
r.activity.mCalled = false;
mInstrumentation.callActivityOnPause(r.activity);
EventLog.writeEvent(LOG_ON_PAUSE_CALLED, r.activity.getComponentName().getClassName());
if (!r.activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onPause()");
}
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to pause activity "
+ r.intent.getComponent().toShortString()
+ ": " + e.toString(), e);
}
}
r.paused = true;
// Notify any outstanding on paused listeners
ArrayList<OnActivityPausedListener> listeners;
synchronized (mOnPauseListeners) {
listeners = mOnPauseListeners.remove(r.activity);
}
int size = (listeners != null ? listeners.size() : 0);
for (int i = 0; i < size; i++) {
listeners.get(i).onPaused(r.activity);
}
return state;
}
위에서 가장 중요한 것은 다음과 같이 호출되었다는 것이다.
mInstrumentation.callActivityOnPause(r.activity);
진입 방법
/**
* Perform calling of an activity‘s {@link Activity#onPause} method. The
* default implementation simply calls through to that method.
*
* @param activity The activity being paused.
*/
public void callActivityOnPause(Activity activity) {
activity.performPause();
}
final void performPause() {
mFragments.dispatchPause();
mCalled = false;
onPause();
mResumed = false;
if (!mCalled && getApplicationInfo().targetSdkVersion
>= android.os.Build.VERSION_CODES.GINGERBREAD) {
throw new SuperNotCalledException(
"Activity " + mComponent.toShortString() +
" did not call through to super.onPause()");
}
mResumed = false;
}
위에서 보면 최종적으로 생명주기 중의 onPause 방법을 호출하였다
두 번째 방법handleDestroyActivity(r.token,false,configChanges,true)를 살펴보자.
private void handleDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = performDestroyActivity(token, finishing,
configChanges, getNonConfigInstance);
if (r != null) {
cleanUpPendingRemoveWindows(r);
WindowManager wm = r.activity.getWindowManager();
View v = r.activity.mDecor;
if (v != null) {
if (r.activity.mVisibleFromServer) {
mNumVisibleActivities--;
}
IBinder wtoken = v.getWindowToken();
if (r.activity.mWindowAdded) {
if (r.onlyLocalRequest) {
// Hold off on removing this until the new activity‘s
// window is being added.
r.mPendingRemoveWindow = v;
r.mPendingRemoveWindowManager = wm;
} else {
wm.removeViewImmediate(v);
}
}
if (wtoken != null && r.mPendingRemoveWindow == null) {
WindowManagerImpl.getDefault().closeAll(wtoken,
r.activity.getClass().getName(), "Activity");
}
r.activity.mDecor = null;
}
if (r.mPendingRemoveWindow == null) {
// If we are delaying the removal of the activity window, then
// we can‘t clean up all windows here. Note that we can‘t do
// so later either, which means any windows that aren‘t closed
// by the app will leak. Well we try to warning them a lot
// about leaking windows, because that is a bug, so if they are
// using this recreate facility then they get to live with leaks.
WindowManagerImpl.getDefault().closeAll(token,
r.activity.getClass().getName(), "Activity");
}
// Mocked out contexts won‘t be participating in the normal
// process lifecycle, but if we‘re running with a proper
// ApplicationContext we need to have it tear down things
// cleanly.
Context c = r.activity.getBaseContext();
if (c instanceof ContextImpl) {
((ContextImpl) c).scheduleFinalCleanup(
r.activity.getClass().getName(), "Activity");
}
}
if (finishing) {
try {
ActivityManagerNative.getDefault().activityDestroyed(token);
} catch (RemoteException ex) {
// If the system process has died, it‘s game over for everyone.
}
}
}
ActivityClientRecord r = performDestroyActivity(token, finishing,
configChanges, getNonConfigInstance);
public final ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing) {
return performDestroyActivity(token, finishing, 0, false);
}
private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance) {
ActivityClientRecord r = mActivities.get(token);
Class activityClass = null;
if (localLOGV) Slog.v(TAG, "Performing finish of " + r);
if (r != null) {
activityClass = r.activity.getClass();
r.activity.mConfigChangeFlags |= configChanges;
if (finishing) {
r.activity.mFinished = true;
}
if (!r.paused) {
try {
r.activity.mCalled = false;
mInstrumentation.callActivityOnPause(r.activity);
EventLog.writeEvent(LOG_ON_PAUSE_CALLED,
r.activity.getComponentName().getClassName());
if (!r.activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + safeToComponentShortString(r.intent)
+ " did not call through to super.onPause()");
}
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to pause activity "
+ safeToComponentShortString(r.intent)
+ ": " + e.toString(), e);
}
}
r.paused = true;
}
if (!r.stopped) {
try {
r.activity.performStop();
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to stop activity "
+ safeToComponentShortString(r.intent)
+ ": " + e.toString(), e);
}
}
r.stopped = true;
}
if (getNonConfigInstance) {
try {
r.lastNonConfigurationInstances
= r.activity.retainNonConfigurationInstances();
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to retain activity "
+ r.intent.getComponent().toShortString()
+ ": " + e.toString(), e);
}
}
}
try {
r.activity.mCalled = false;
mInstrumentation.callActivityOnDestroy(r.activity);
if (!r.activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + safeToComponentShortString(r.intent) +
" did not call through to super.onDestroy()");
}
if (r.window != null) {
r.window.closeAllPanels();
}
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to destroy activity " + safeToComponentShortString(r.intent)
+ ": " + e.toString(), e);
}
}
}
mActivities.remove(token);
StrictMode.decrementExpectedActivityCount(activityClass);
return r;
}
위 코드에서 중요한 호출은 다음과 같습니다.
r.activity.performStop();및 mInstrumentation.callActivityOnDestroy(r.activity);
이 두 가지 방법은 stop의 현재 Activity와 destroy Activity에 중점을 두고 있습니다.
세 번째 방법handle Launch Activity로 돌아가겠습니다.
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// If we are getting ready to gc after going to the background, well
// we are back active so skip it.
unscheduleGcIdler();
if (r.profileFd != null) {
mProfiler.setProfiler(r.profileFile, r.profileFd);
mProfiler.startProfiling();
mProfiler.autoStopProfiler = r.autoStopProfiler;
}
// Make sure we are running with the most recent config.
handleConfigurationChanged(null, null);
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward);
if (!r.activity.mFinished && r.startsNotResumed) {
// The activity manager actually wants this one to start out
// paused, because it needs to be visible but isn‘t in the
// foreground. We accomplish this by going through the
// normal startup (because activities expect to go through
// onResume() the first time they run, before their window
// is displayed), and then pausing it. However, in this case
// we do -not- need to do the full pause cycle (of freezing
// and such) because the activity manager assumes it can just
// retain the current state it has.
try {
r.activity.mCalled = false;
mInstrumentation.callActivityOnPause(r.activity);
// We need to keep around the original state, in case
// we need to be created again.
r.state = oldState;
if (!r.activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onPause()");
}
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(r.activity, e)) {
throw new RuntimeException(
"Unable to pause activity "
+ r.intent.getComponent().toShortString()
+ ": " + e.toString(), e);
}
}
r.paused = true;
}
} else {
// If there was an error, for any reason, tell the activity
// manager to stop us.
try {
ActivityManagerNative.getDefault()
.finishActivity(r.token, Activity.RESULT_CANCELED, null);
} catch (RemoteException ex) {
// Ignore
}
}
}
가령 우리가handleLaunchActivity 방법을 주석해 버리면 현재 인터페이스가 소각되고 Activity가 일어나지 않으면 블랙스크린이 될 것이다. 허허
위의 중요한 두 가지 방법은 다음과 같습니다.
1、 Activity a = performLaunchActivity(r, customIntent);
2、 handleResumeActivity(r.token, false, r.isForward);
PerformLaunchActivity 방법을 먼저 살펴보겠습니다.
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
ActivityInfo aInfo = r.activityInfo;
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);
}
ComponentName component = r.intent.getComponent();
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
if (localLOGV) Slog.v(
TAG, r + ": app=" + app
+ ", appName=" + app.getPackageName()
+ ", pkg=" + r.packageInfo.getPackageName()
+ ", comp=" + r.intent.getComponent().toShortString()
+ ", dir=" + r.packageInfo.getAppDir());
if (activity != null) {
ContextImpl appContext = new ContextImpl();
appContext.init(r.packageInfo, r.token, this);
appContext.setOuterContext(activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config);
if (customIntent != null) {
activity.mIntent = customIntent;
}
r.lastNonConfigurationInstances = null;
activity.mStartedActivity = false;
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}
activity.mCalled = false;
mInstrumentation.callActivityOnCreate(activity, r.state);
if (!activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onCreate()");
}
r.activity = activity;
r.stopped = true;
if (!r.activity.mFinished) {
activity.performStart();
r.stopped = false;
}
if (!r.activity.mFinished) {
if (r.state != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
if (!r.activity.mFinished) {
activity.mCalled = false;
mInstrumentation.callActivityOnPostCreate(activity, r.state);
if (!activity.mCalled) {
throw new SuperNotCalledException(
"Activity " + r.intent.getComponent().toShortString() +
" did not call through to super.onPostCreate()");
}
}
}
r.paused = true;
mActivities.put(r.token, r);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to start activity " + component
+ ": " + e.toString(), e);
}
}
return activity;
}
위의 중요한 코드 중 하나:
mInstrumentation.callActivityOnCreate(activity, r.state);
public void callActivityOnCreate(Activity activity, Bundle icicle) {
if (mWaitingActivities != null) {
synchronized (mSync) {
final int N = mWaitingActivities.size();
for (int i=0; i<N; i++) {
final ActivityWaiter aw = mWaitingActivities.get(i);
final Intent intent = aw.intent;
if (intent.filterEquals(activity.getIntent())) {
aw.activity = activity;
mMessageQueue.addIdleHandler(new ActivityGoing(aw));
}
}
}
}
activity.performCreate(icicle);
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
am.match(activity, activity, activity.getIntent());
}
}
}
}
진입 방법performCreate
final void performCreate(Bundle icicle) {
onCreate(icicle);
mVisibleFromClient = !mWindow.getWindowStyle().getBoolean(
com.android.internal.R.styleable.Window_windowNoDisplay, false);
mFragments.dispatchActivityCreated();
}
Oncreate를 호출해서 시작하는 걸 봤어요.
두 번째 방법handle ResumeActivity로 돌아가서 위에서 한 걸음 한 걸음 추적해서 Resume를 호출했습니다.
위에서 보면 다음과 같은 몇 가지 요약이 필요합니다.
설명: 입력법의 국제화는 cb에Activity도 있고 서비스도 있다. 입력법은 서비스이고 Input Method Service의 계승 클래스에 onConfiguration Changed 방법을 추가했기 때문에 onConfiguration Changed 방법을 되돌려준다. 나는 이 방법에서kill 동작을 했다. 위 코드와유사한Activity의 재부팅 방법이다.
cb.onConfigurationChanged(config);
public void KillPinyin(){
android.os.Process.killProcess(android.os.Process.myPid());
final ActivityManager am = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
am.restartPackage(getPackageName());
}
지금 궁금한 게 있는데, 만약에 내가 onConfigurationChanged를 다시 덮어쓰지 않는다면 어떻게 될까요?허허, 규칙에 따라 덮어쓰지 않는 방법은 다른 응용처럼 자동적으로 국가화될 것이다. 그럴 수 있을까?그것은 할 수 없는 것이니, 믿지 못하겠으면 해 봐라, 왜?이것은 또 다른 과제이다. 허허, 나도 누군가가 나와 이것을 교류하고 싶다. 즉, 왜 이 방법을 덮어쓰지 않아도 안 되는가?나 는 지금 블로그 를 쓸 시간 이 없어서 프로젝트 를 해야 한다
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
다양한 언어의 JSONJSON은 Javascript 표기법을 사용하여 데이터 구조를 레이아웃하는 데이터 형식입니다. 그러나 Javascript가 코드에서 이러한 구조를 나타낼 수 있는 유일한 언어는 아닙니다. 저는 일반적으로 '객체'{}...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.