AMS startActivity()

栏目: Java · 发布时间: 5年前

内容简介:问题ActivityStartController ActivityStarter 是什么?作用?关系?frameworks/base/services/java/com/android/server/SystemServer.java

参考:

Android应用程序的Activity启动过程简要介绍和学习计划

https://blog.csdn.net/luoshen...

Android深入四大组件(六)Android8.0 根Activity启动过程(前篇)

https://blog.csdn.net/itachi8...

问题

ActivityStartController ActivityStarter 是什么?作用?关系?

  1. AMS init

frameworks/base/services/java/com/android/server/SystemServer.java

  1. startActivity --> OnCreate()

frameworks/base/core/java/android/app/ContextImpl.java

有几个方法可startActivity(), 最终都调到ActivityManagerService.java里

eg:

startActivity() --> mMainThread.getInstrumentation().execStartActivity() --> startActivity()/ActivityManagerService.java --> startActivityAsUser()

startActivityAsUser() --> ... startActivityAsUser()/ActivityManagerService.java

startActivityAsUser()/ActivityManagerService.java

public final int startActivityAsUser(IApplicationThread caller, String callingPackage,

Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
    int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId,
    boolean validateIncomingUser) {
//判断调用者进程是否被隔离   
enforceNotIsolatedCaller("startActivity");

userId = mActivityStartController.checkTargetUser(userId, validateIncomingUser,
        Binder.getCallingPid(), Binder.getCallingUid(), "startActivityAsUser");

// TODO: Switch to user app stacks here.
return mActivityStartController.obtainStarter(intent, "startActivityAsUser")
        .setCaller(caller)
        .setCallingPackage(callingPackage)
        .setResolvedType(resolvedType)
        .setResultTo(resultTo)
        .setResultWho(resultWho)
        .setRequestCode(requestCode)
        .setStartFlags(startFlags)
        .setProfilerInfo(profilerInfo)
        .setActivityOptions(bOptions)
        .setMayWait(userId)
        .execute();

}

这里通过 ActivityStartController 申请了个 ActivityStarter,然后执行

frameworks/base/services/core/java/com/android/server/am/ActivityStarter.java

maywait由上面setMayWait(userId)设置, 然后调用 startActivityMayWait() 或者 startActivity()

执行完后onExecutionComplete()回调ActivityStartController的 onExecutionComplete()执行一些回收任务;

/**

  • Starts an activity based on the request parameters provided earlier.
  • @return The starter result.

*/

int execute() {

try {
    // TODO(b/64750076): Look into passing request directly to these methods to allow
    // for transactional diffs and preprocessing.
    if (mRequest.mayWait) {
        return startActivityMayWait(mRequest.caller, mRequest.callingUid,
                mRequest.callingPackage, mRequest.intent, mRequest.resolvedType,
                mRequest.voiceSession, mRequest.voiceInteractor, mRequest.resultTo,
                mRequest.resultWho, mRequest.requestCode, mRequest.startFlags,
                mRequest.profilerInfo, mRequest.waitResult, mRequest.globalConfig,
                mRequest.activityOptions, mRequest.ignoreTargetSecurity, mRequest.userId,
                mRequest.inTask, mRequest.reason,
                mRequest.allowPendingRemoteAnimationRegistryLookup);
    } else {
        return startActivity(mRequest.caller, mRequest.intent, mRequest.ephemeralIntent,
                mRequest.resolvedType, mRequest.activityInfo, mRequest.resolveInfo,

......

}
} finally {
    onExecutionComplete();
}

private int startActivityMayWait(IApplicationThread caller, int callingUid,

String callingPackage, Intent intent, String resolvedType,
    IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
    IBinder resultTo, String resultWho, int requestCode, int startFlags,
    ProfilerInfo profilerInfo, WaitResult outResult,
    Configuration globalConfig, SafeActivityOptions options, boolean ignoreTargetSecurity,
    int userId, TaskRecord inTask, String reason,
    boolean allowPendingRemoteAnimationRegistryLookup) {

......

final ActivityRecord[] outRecord = new ActivityRecord[1];
    int res = startActivity(caller, intent, ephemeralIntent, resolvedType, aInfo, rInfo,
            voiceSession, voiceInteractor, resultTo, resultWho, requestCode, callingPid,
            callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, options,
            ignoreTargetSecurity, componentSpecified, outRecord, inTask, reason,
            allowPendingRemoteAnimationRegistryLookup);

......

if (outResult != null) {
        outResult.result = res;

        final ActivityRecord r = outRecord[0];
        //根据返回结果进一步处理
        switch(res) {
            case START_SUCCESS: {

......

case START_DELIVERED_TO_TOP: {

......

case START_TASK_TO_FRONT: {

......

上面的caller为IApplicationThread

startActivity(IApplicationThread..)也有两个,最终调用到了startActivity(final ActivityRecord r..)

private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,

String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,

...... //创建即将要启动的Activity的描述类ActivityRecord

ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
        callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
        resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
        mSupervisor, checkedOptions, sourceRecord);

......

return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
        true /* doResume */, checkedOptions, inTask, outActivity);

}

startActivity(IApplicationThread..) --> startActivity(final ActivityRecord r..)

private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord,

IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
        int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
        ActivityRecord[] outActivity) {
int result = START_CANCELED;
try {
    mService.mWindowManager.deferSurfaceLayout();
    result = startActivityUnchecked(r, sourceRecord, voiceSession, voiceInteractor,
            startFlags, doResume, options, inTask, outActivity);
} finally {

......

}

postStartActivityProcessing(r, result, mTargetStack);

return result;

}

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,

IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
    int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
    ActivityRecord[] outActivity) {

......

if (reusedActivity != null) {
    ......
    reusedActivity = setTargetStackAndMoveToFrontIfNeeded(reusedActivity);

.......

//是否要创建新的task
// Should this be considered a new task?
int result = START_SUCCESS;
if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
        && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {
    newTask = true;
    result = setTaskFromReuseOrCreateNewTask(taskToAffiliate, topStack);  //<----
} else if (mSourceRecord != null) {
    result = setTaskFromSourceRecord();
} else if (mInTask != null) {
    result = setTaskFromInTask();
} else {
    // This not being started from an existing activity, and not part of a new task...
    // just put it in the top task, though these days this case should never happen.
    setTaskToCurrentTopOrCreateNewTask();
}

......

mTargetStack.startActivityLocked(mStartActivity, topFocused, newTask, mKeepCurTransition,
        mOptions);
if (mDoResume) {
    final ActivityRecord topTaskActivity =
            mStartActivity.getTask().topRunningActivityLocked();
    if (!mTargetStack.isFocusable()
            || (topTaskActivity != null && topTaskActivity.mTaskOverlay
            && mStartActivity != topTaskActivity)) {
        // If the activity is not focusable, we can't resume it, but still would like to
        // make sure it becomes visible as it starts (this will also trigger entry
        // animation). An example of this are PIP activities.
        // Also, we don't want to resume activities in a task that currently has an overlay
        // as the starting activity just needs to be in the visible paused state until the
        // over is removed.
        mTargetStack.ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
        // Go ahead and tell window manager to execute app transition for this activity
        // since the app transition will not be triggered through the resume channel.
        mService.mWindowManager.executeAppTransition();
    } else {
        // If the target stack was not previously focusable (previous top running activity
        // on that stack was not visible) then any prior calls to move the stack to the
        // will not update the focused stack.  If starting the new activity now allows the
        // task stack to be focusable, then ensure that we now update the focused stack
        // accordingly.
        if (mTargetStack.isFocusable() && !mSupervisor.isFocusedStack(mTargetStack)) {
            mTargetStack.moveToFront("startActivityUnchecked");
        }
        mSupervisor.resumeFocusedStackTopActivityLocked(mTargetStack, mStartActivity,
                mOptions);
    }
} else if (mStartActivity != null) {
    mSupervisor.mRecentTasks.add(mStartActivity.getTask());
}
mSupervisor.updateUserStackLocked(mStartActivity.userId, mTargetStack);

mSupervisor.handleNonResizableTaskIfNeeded(mStartActivity.getTask(), preferredWindowingMode,
        preferredLaunchDisplayId, mTargetStack);

return START_SUCCESS;

}

在注释1处我们得知,启动根Activity时会将Intent的Flag设置为FLAG_ACTIVITY_NEW_TASK,

这样注释1处的条件判断就会满足,

接着执行注释2处的 setTaskFromReuseOrCreateNewTask(),其内部会创建一个新的TaskRecord,

TaskRecord用来描述一个Activity任务栈,也就是说setTaskFromReuseOrCreateNewTask()内部会创建一个新的Activity任务栈

boolean resumeFocusedStackTopActivityLocked(

ActivityStack targetStack, ActivityRecord target, ActivityOptions targetOptions) {

......

if (targetStack != null && isFocusedStack(targetStack)) {
    return targetStack.resumeTopActivityUncheckedLocked(target, targetOptions);
}

......//获取要启动的Activity所在栈的栈顶的不是处于停止状态的ActivityRecord

final ActivityRecord r = mFocusedStack.topRunningActivityLocked();//1
if (r == null || !r.isState(RESUMED)) {//2
    mFocusedStack.resumeTopActivityUncheckedLocked(null, null);//3
} else if (r.isState(RESUMED)) {
    // Kick off any lingering app transitions form the MoveTaskToFront operation.
    mFocusedStack.executeAppTransition(targetOptions);
}

注释1处调用ActivityStack的topRunningActivityLocked方法获取要启动的Activity所在栈的栈顶的不是处于停止状态的ActivityRecord。

注释2处如果ActivityRecord不为null,或者要启动的Activity的状态不是RESUMED状态,

就会调用注释3处的ActivityStack的resumeTopActivityUncheckedLocked方法,对于即将要启动的Activity,

注释2的条件判断是肯定满足,因此我们来查看ActivityStack的resumeTopActivityUncheckedLocked方法

frameworks/base/services/core/java/com/android/server/am/ActivityStack.java

mFocusedStack.resumeTopActivityUncheckedLocked() --> resumeTopActivityInnerLocked() --> mStackSupervisor.startSpecificActivityLocked(next, true, false);

frameworks/base/services/core/java/com/android/server/am/ActivityStackSupervisor.java

void startSpecificActivityLocked(ActivityRecord r,

boolean andResume, boolean checkConfig) {
// Is this activity's application already running?
ProcessRecord app = mService.getProcessRecordLocked(r.processName,
        r.info.applicationInfo.uid, true);

....

if (app != null && app.thread != null) {

......//如果app已经运行,调用realStartActivityLocked()

realStartActivityLocked(r, app, andResume, checkConfig);
        return;

......

}

//如果没有运行则调用AMS的startProcessLocked()

mService.startProcessLocked(r.processName, r.info.applicationInfo, true, 0,
        "activity", r.intent.getComponent(), false, false, true);

}

realStartActivityLocked()为应用程序内部启动非默认Activity的过程,暂时不看,

我们看 startProcessLocked()

startProcessLocked()函数也有好几个, 最终都会转为 startProcessLocked(ProcessRecord app...

ProcessRecord生成eg:

final ProcessRecord r = new ProcessRecord(this, stats, info, proc, uid);

然后进一步的调用

startProcessLocked(String hostingType,... --> startProcess(app.hostingType...

private ProcessStartResult startProcess(String hostingType, String entryPoint,

ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,

......

if (hostingType.equals("webview_service")) {
        startResult = startWebView(entryPoint,
                app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                app.info.dataDir, null,
                new String[] {PROC_START_SEQ_IDENT + app.startSeq});
    } else {
        startResult = Process.start(entryPoint,
                app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                app.info.dataDir, invokeWith,
                new String[] {PROC_START_SEQ_IDENT + app.startSeq});
    }

如果hostingType为webview_service调用Process.startWebView()

否则 Process.start()

frameworks/base/core/java/android/os/Process.java

  • @param processClass The class to use as the process's main entry
  • point.

public static final ProcessStartResult start(final String processClass,

final String niceName,
                          int uid, int gid, int[] gids,
                          int runtimeFlags, int mountExternal,
                          int targetSdkVersion,
                          ......) {
// 注意 processClass 为 "android.app.ActivityThread"
return zygoteProcess.start(processClass, niceName, uid, gid, gids,
            runtimeFlags, mountExternal, targetSdkVersion, seInfo,
            abi, instructionSet, appDataDir, invokeWith, zygoteArgs);

}

我们重点关注下传入的参数 processClass,即 entryPoint,通过查找传传下来的参数,即为

final String entryPoint = "android.app.ActivityThread";

新的进程会导入android.app.ActivityThread类,并且执行它的main函数.

zygoteProcess.start()后面有机会再分析,我们就直接看 ActivityThread main()

frameworks/base/core/java/android/app/ActivityThread.java

public static void main(String[] args) {

......

Looper.prepareMainLooper();

// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
// It will be in the format "seq=114"
long startSeq = 0;
if (args != null) {
    for (int i = args.length - 1; i >= 0; --i) {
        if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
            startSeq = Long.parseLong(
                    args[i].substring(PROC_START_SEQ_IDENT.length()));
        }
    }
}
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);

if (sMainThreadHandler == null) {
    sMainThreadHandler = thread.getHandler();
}

......

Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");

}

其main函数主要为Looper准备主looper, 创建一个ActivityThread实例,然后调用它的attach函数,

接着就进入消息循环了,若loope退出则抛个异常。

attach()如果为非system则进一步调用AMS attachApplication()

private void attach(boolean system, long startSeq) {

......

if (!system) {
    final IActivityManager mgr = ActivityManager.getService();
    try {
        mgr.attachApplication(mAppThread, startSeq);

attachApplication()--> attachApplicationLocked()

private final boolean attachApplicationLocked(IApplicationThread thread,

int pid, int callingUid, long startSeq) {
    } else if (app.instr != null) {
        thread.bindApplication(processName, appInfo, providers,

..

// See if the top visible activity is waiting to run in this process...
if (normalMode) {
    try {
        if (mStackSupervisor.attachApplicationLocked(app)) {

thread.bindApplication() 将应用进程的ApplicationThread对象绑定到ActivityManagerService,

也就是说获得ApplicationThread对象的代理对象。

mStackSupervisor.attachApplicationLocked(app)

frameworks/base/services/core/java/com/android/server/am/ActivityStackSupervisor.java#

boolean attachApplicationLocked(ProcessRecord app) throws RemoteException {

......

final ActivityRecord top = stack.topRunningActivityLocked();
        final int size = mTmpActivityList.size();
        for (int i = 0; i < size; i++) {
            final ActivityRecord activity = mTmpActivityList.get(i);
            if (activity.app == null && app.uid == activity.info.applicationInfo.uid
                    && processName.equals(activity.processName)) {
                try {
                    if (realStartActivityLocked(activity, app,
                            top == activity /* andResume */, true /* checkConfig */)) {

......

}

final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app,

boolean andResume, boolean checkConfig) throws RemoteException {

.........

// Create activity launch transaction.
         final ClientTransaction clientTransaction = ClientTransaction.obtain(app.thread,
                 r.appToken);
         clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent), <----注意这个callback,为LaunchActivityItem
                 System.identityHashCode(r), r.info,

.......

// Schedule transaction.
         mService.getLifecycleManager().scheduleTransaction(clientTransaction);

Android9引入了ClientLifecycle和ClientTransactionHandler来辅助管理Activity生命周期,过程更麻烦了.

frameworks/base/services/core/java/com/android/server/am/ClientLifecycleManager.java

void scheduleTransaction(ClientTransaction transaction) throws RemoteException {

final IApplicationThread client = transaction.getClient();
transaction.schedule(); -->

frameworks/base/core/java/android/app/servertransaction/ClientTransaction.java

private IApplicationThread mClient;

public void schedule() throws RemoteException {

mClient.scheduleTransaction(this); -->

}

frameworks/base/core/java/android/app/ActivityThread.java

private class ApplicationThread extends IApplicationThread.Stub {

......

@Override
 public void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
     ActivityThread.this.scheduleTransaction(transaction); -->
 }

ActivityThread类中并没有定义scheduleTransaction方法,所以调用的是他父类ClientTransactionHandler的 scheduleTransaction()。

frameworks/base/core/java/android/app/ClientTransactionHandler.java

void scheduleTransaction(ClientTransaction transaction) {

transaction.preExecute(this);
sendMessage(ActivityThread.H.EXECUTE_TRANSACTION, transaction);

}

接到Handler后

frameworks/base/core/java/android/app/ActivityThread.java

public void handleMessage(Message msg) {

case EXECUTE_TRANSACTION:
        final ClientTransaction transaction = (ClientTransaction) msg.obj;
        mTransactionExecutor.execute(transaction); -->

frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java

public void execute(ClientTransaction transaction) {

......

executeCallbacks(transaction);

executeLifecycleState(transaction);

上面会执行callback函数,然后再执行executeLifecycleState()

这里我们只看callback函数,注意

realStartActivityLocked()传入的callback为LaunchActivityItem

public void executeCallbacks(ClientTransaction transaction) {

final List<ClientTransactionItem> callbacks = transaction.getCallbacks();

......

final int size = callbacks.size();
for (int i = 0; i < size; ++i) {
    final ClientTransactionItem item = callbacks.get(i);

......

item.execute(mTransactionHandler, token, mPendingActions); -->
    item.postExecute(mTransactionHandler, token, mPendingActions);

......

}

item.execute() 即调用到LaunchActivityItem.execute()

frameworks/base/core/java/android/app/servertransaction/LaunchActivityItem.java

execute() --> client.handleLaunchActivity -->

ActivityThread.java handleLaunchActivity() --> performLaunchActivity()

在ActivityThread.performLaunchActivity方法中首先对Activity的ComponentName、ContextImpl、Activity

以及Application对象进行了初始化并相互关联,

然后设置Activity主题,最后调用Instrumentation.callActivityOnCreate方法。

/ * Core implementation of activity launch. /

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

......//初始化ComponentName

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);
}
//初始化ContextImpl
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
    java.lang.ClassLoader cl = appContext.getClassLoader();
    //初始化activity
    activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
   ......
}

try {//初始化Application
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);

......

if (activity != null) {

....... //关联

appContext.setOuterContext(activity);
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor, window, r.configCallback);

......//调用callActivityOnCreate

if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }

......

}
    //状态设为ON_CREATE
    r.setState(ON_CREATE);

......

return activity;

}

mInstrumentation.callActivityOnCreate() --> activity.performCreate -->

frameworks/base/core/java/android/app/Activity.java

final void performCreate(Bundle icicle, PersistableBundle persistentState) {

......

if (persistentState != null) {
    onCreate(icicle, persistentState);
} else {
    onCreate(icicle);
}

......

}

即最终调用到了我们的应用程序的onCreate()

https://www.websequencediagra...

Title Androd9 startActivity->OnCreate流程

participant ContextImpl
participant Instrumentation
participant ActivityManagerService
participant ActivityStartController
participant ActivityStarter
participant ActivityStack
participant ActivityStackSupervisor
participant Process
participant zygoteProcess
participant ActivityThread


opt startActivity part1
    ContextImpl->+Instrumentation:startActivity() --> mMainThread.getInstrumentation()
        Instrumentation->ActivityManagerService:execStartActivity()
        ActivityManagerService->ActivityStartController:startActivityAsUser()
        ActivityStartController->ActivityStarter:obtainStarter()
        ActivityStarter->ActivityStarter:execute()
        ActivityStarter->ActivityStarter:startActivityMayWait()/startActivity(IApplicationThread...)
        ActivityStarter->ActivityStarter:startActivity(IApplicationThread...)
        ActivityStarter->ActivityStarter:startActivity(ActivityRecord...)
        ActivityStarter->+ActivityStarter:startActivityUnchecked(ActivityRecord...)
            ActivityStarter->ActivityStarter:setTaskFromReuseOrCreateNewTask()
            ActivityStarter->ActivityStackSupervisor:resumeFocusedStackTopActivityLocked()
            ActivityStackSupervisor->ActivityStack:resumeTopActivityInnerLocked()
            ActivityStack->ActivityStackSupervisor:startSpecificActivityLocked()
            alt if (app.thread != null)
                ActivityStackSupervisor->ActivityStackSupervisor:realStartActivityLocked()
            else
                ActivityStackSupervisor->ActivityManagerService:startProcessLocked()
            end
            ActivityManagerService->ActivityManagerService:startProcessLocked(ProcessRecord app...
            ActivityManagerService->ActivityManagerService:startProcessLocked(String hostingType,...
            ActivityManagerService->ActivityManagerService:startProcess(app.hostingType...
            alt 如果hostingType为webview_service
                ActivityManagerService->Process:startWebView()
            else
                ActivityManagerService->Process:start("android.app.ActivityThread"...)
            end
end

opt startActivity part2
            Process->zygoteProcess:start() 
            zygoteProcess->ActivityThread:...->android.app.ActivityThread main()

            note right of ActivityThread
                Looper.prepareMainLooper();
                ActivityThread thread = new ActivityThread();
                thread.attach(false, startSeq);
                Looper.loop();
            end note
end

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

产品型社群

产品型社群

李善友 / 机械工业出版社 / 2015-3-1 / CNY 69.00

传统模式企业正在直面一场空前的“降维战争”, 结局惨烈,或生或死。 传统模式很难避免悲惨下场, 诺基亚等昔日庞然大物轰然倒塌, 柯达发明了数码成像技术却依然破产, 新商业的兴起到底遵循的是什么模式? 微信轻而易举干掉了运营商的短信业务, “好未来”为何让传统教育不明觉厉? 花间堂为什么不是酒店,而是入口? 将来不会有互联网企业与传统企业之分, ......一起来看看 《产品型社群》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具