本文將分析Android6.0中下拉狀態欄快捷開關QSTitle組件的創建流程,從開機init過程開始至具體的每個QSTitle對象具體的創建,如何添加入下拉狀態欄,對QSTitle的相關整體流程進行梳理。
android設備上電,引導程序引導進入boot(通常是uboot),加載initramfs、kernel鏡像,啟動kernel后,進入用戶態程序。第一個用戶空間程序是init, PID固定是1.
在android系統上,init.cpp的代碼位于/system/core/init下,基本功能有:
- 管理設備
- 解析并處理Android啟動腳本init.rc
- 實時維護這個init.rc中的服務,包括加載 Zygote
這里不過過多分析init.cpp部分,重點關注Zygote的啟動流程。init,cpp部分推薦相關文檔http://my.oschina.net/youranhongcha/blog/469028
init.rc配置文件在 Android 6.0 代碼中位于 system/core/rootdir
/init.zygote32.rc,具體內容為:
service zygote /system/bin/app_process -Xzygote /system/bin --zygote --start-system-server
class main
socket zygote stream 660 root system
onrestart write /sys/android_power/request_state wake
onrestart write /sys/power/state on
onrestart restart media
onrestart restart netd
writepid /dev/cpuset/foreground/tasks
其中:
- 關鍵字 service 表示告訴 init 進程創建名為 zygote 的進程,所要執行的程序是 /system/bin/app_process,后面的都是傳遞的參數。
- 注意參數 --start-system-server,說明要啟動 SystemServer
- socket zygote stream 660 root system 表示創建名為 zygote 的 socket。
- 后面的 onrestart 關鍵字表示 zygote 進程重啟時所需執行的命令。
Zygote 的啟動
從中我們可以得出結論: zygote 只是服務的名稱,與此服務對應的程序是 app_process 程序,我們研究 zygote 的實現,就是要研究 app_process 程序。
app_process 代碼位于 frameworks/base/cmds/app_process
/app_main.cpp,入口函數為 main。
main 的大體流程為:
- 創建一個 AppRuntime 實例 runtime
- 解析傳入的命令行參數
- 判斷調用哪一個 runtime.start,傳入對應的參數
- 由于 init.zygote32.rc 中傳入參數 -Xzygote /system/bin --zygote --start-system-server,因此將執行:
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
由此可知,app_process 沒干什么主要的事情,只是跳轉到 Java 類 com.android.internal.os.ZygoteInit,看來工作都在 ZygoteInit 中完成。但ZygoteInit如何啟動呢?
我們來看看AppRuntime 。
AndroidRuntime
runtime.start方法來自于 AppRuntime 的父類 AndroidRuntime,代碼位于 frameworks/base/core/jni/AndroidRuntime.cpp。重點看看 AndroidRuntime start 方法,它主要做了三件事:
首先是創建虛擬機:
/* start the virtual machine */
JniInvocation jni_invocation;
jni_invocation.Init(NULL);
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote) != 0) {
return;
}
onVmCreated(env);
之后是調用 startReg 函數注冊 JNI 方法:
/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
最后構建參數、創建 JNI 類對象,獲取 main 方法,并最終執行下面一行執行 main 入口:
env->CallStaticVoidMethod(startClass, startMeth, strArray);
這樣我們就成功從 AndroidRuntime.cpp中啟動了一個虛擬機,并加載了 Java 類 ZygoteInit,并進入到它的 main 方法中執行。
ZygoteInit
之后就來到了 ZygoteInit.java 的 main 方法,代碼位frameworks/
base/core/java/com/android/internal/os/ZygoteInit.java。來看其 main 方法:
public static void main(String argv[]) {
try {
...
registerZygoteSocket(socketName);
...
if (startSystemServer) {
startSystemServer(abiList, socketName);
}
...
runSelectLoop(abiList);
...
} catch (...) { ... }
}
首先,調用 registerZygoteSocket 創建了一個名為 zygote 的 socket:
registerZygoteSocket(socketName);
之后啟動 SystemServer 組件:
if (startSystemServer) {
startSystemServer(abiList, socketName);
}
最后調用 runSelectLoopMode 進入一個死循環,等待接受 socket 上由 ActivityManagerService 發來的請求創建應用程序的請求:
runSelectLoop(abiList);
對后startSystemServer(abiList, socketName);進行分析:
啟動 SystemServer 組件
SystemServer 名為系統服務進程,負責啟動 Android 系統的關鍵服務。來看看函數的主要實現:
private static boolean startSystemServer(String abiList, String socketName)
throws MethodAndArgsCaller, RuntimeException {
...
/* Request to fork the system server process */
pid = Zygote.forkSystemServer(
parsedArgs.uid, parsedArgs.gid,
parsedArgs.gids,
parsedArgs.debugFlags,
null,
parsedArgs.permittedCapabilities,
parsedArgs.effectiveCapabilities);
...
/* For child process */
if (pid == 0) {
...
handleSystemServerProcess(parsedArgs);
}
return true;
}
這里調用了 Zygote 的靜態方法 forkSystemServer 來創建 SystemServer 進程。
在forkSystemServer 中進行JNI調用forkSystemServer - com_android_internal_os_Zygote_nativeForkSystemServer - ForkAndSpecializeCommon來成功fork一個新進程。
子進程創建好之后,有一句
handleSystemServerProcess(parsedArgs);
進入handleSystemServerProcess:
/**
* Finish remaining work for the newly forked system server process.
*/
private static void handleSystemServerProcess(
ZygoteConnection.Arguments parsedArgs)
throws ZygoteInit.MethodAndArgsCaller {
...
final String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH");
...
if (...) {
...
} else {
ClassLoader cl = null;
if (systemServerClasspath != null) {
cl = new PathClassLoader(systemServerClasspath, ClassLoader.getSystemClassLoader());
Thread.currentThread().setContextClassLoader(cl);
}
/*
* Pass the remaining arguments to SystemServer.
*/
RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
}
從中可以看出,我們從環境變量 SYSTEMSERVERCLASSPATH 拿到 SystemServer 的類名,之后載入進來,最后使用 RuntimeInit.zygoteInit 來運行,它來執行 SystemServer 的 main 方法。
看一下RuntimeInit.zygoteInit():
public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
throws ZygoteInit.MethodAndArgsCaller {
...
commonInit(); // 基本設置(異常捕獲、時區、HTTP User-Agent 等)
nativeZygoteInit();
applicationInit(targetSdkVersion, argv, classLoader); // 調用 Main 方法
}
其中 nativeZygoteInit() 是一個 jni 調用,位于 AndroidRuntime.cpp:
static void com_android_internal_os_RuntimeInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{
gCurRuntime->onZygoteInit();
}
onZygoteInit() 是 AppRuntime 中的方法,具體為:
virtual void onZygoteInit()
{
sp<ProcessState> proc = ProcessState::self();
ALOGV("App process: starting thread pool.\n");
proc->startThreadPool();
}
啟動了一個線程池。nativeZygoteInit() 就分析到這里,由此可見,Zygote 啟動 SystemServer 的過程就算完了,之后的,都是 SystemServer 內部的事情了。
SystemServer.main()
上面說到RuntimeInit.zygoteInit 來執行 SystemServer.main方法。我們來看SystemServer.main():
/**
* The main entry point from zygote.
*/
public static void main(String[] args) {
new SystemServer().run();
}
生成SystemServer對象并跳轉到了run();
去run()看看:
private void run() {
......
// Start services.
try {
startBootstrapServices();
startCoreServices();
startOtherServices();
......
} catch (Throwable ex) {
...
throw ex;
}
...
// Loop forever.
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
在這里啟動了一些服務,我們重點去看startBootstrapServices();
private void startBootstrapServices() {
......
Installer installer = mSystemServiceManager.startService(Installer.class);
// Activity manager runs the show.
mActivityManagerService = mSystemServiceManager.startService(
ActivityManagerService.Lifecycle.class).getService();
mActivityManagerService.setSystemServiceManager(mSystemServiceManager);
......
}
Activity manager運行
再回到SystemServer,run()有這么一句:
startOtherServices();
startOtherServices()代碼:
private void startOtherServices() {
final Context context = mSystemContext;
AccountManagerService accountManager = null;
ContentService contentService = null;
.......
mActivityManagerService.systemReady(new Runnable() {
@Override
public void run() {
......
try {
startSystemUi(context);
} catch (Throwable e) {
reportWtf("starting System UI", e);
}
.......
在這里mActivityManagerService.systemReady創建線程去調用startSystemUi(context);
static final void startSystemUi(Context context) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SystemUIService"));
//Slog.d(TAG, "Starting service: " + intent);
context.startServiceAsUser(intent, UserHandle.OWNER);
}
通過intent.setComponent(new ComponentName("com.android.systemui",
"com.android.systemui.SystemUIService"));
設置啟動systemui程序的SystemUIService
進入SystemUIService:
public class SystemUIService extends Service {
@Override
public void onCreate() {
super.onCreate();
((SystemUIApplication) getApplication()).startServicesIfNeeded();
}
......
onCreate方法中獲得SystemUIApplication對象并調用其startServicesIfNeeded方法:
public void startServicesIfNeeded() {
final int N = SERVICES.length;
for (int i=0; i<N; i++) {
Class<?> cl = SERVICES[i];
try {
mServices[i] = (SystemUI)cl.newInstance();//加載實例
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
} catch (InstantiationException ex) {
throw new RuntimeException(ex);
}
mServices[i].mContext = this;
mServices[i].mComponents = mComponents;
mServices[i].start();//start服務
if (mBootCompleted) {
mServices[i].onBootCompleted();
}
}
mServicesStarted = true;
}
這個方法中,首先判斷mServicesStarted標志為來判斷SystemUI相關的服務是否啟動,
同時根據系統配置文件來檢查ActivityManagerService是否finishBooting。
可以看到這里有個mServices數組,并通過for循環加載它們的實例并調用它們的start();
但是mServices數組具體開啟了哪些服務呢?
來看看SystemUIApplication類中的變量:
private final Class<?>[] SERVICES = new Class[] {
com.android.systemui.tuner.TunerService.class,
com.android.systemui.keyguard.KeyguardViewMediator.class,
com.android.systemui.recents.Recents.class,
com.android.systemui.volume.VolumeUI.class,
com.android.systemui.statusbar.SystemBars.class,
com.android.systemui.usb.StorageNotification.class,
com.android.systemui.power.PowerUI.class,
com.android.systemui.media.RingtonePlayer.class,
com.android.systemui.keyboard.KeyboardUI.class,
};
可以看到這里有很多sysytemui中常用的服務,后面將重點分析SystemBars及TunerService。
直接進入SystemBars.start():
public void start() {
mServiceMonitor = new ServiceMonitor(TAG, DEBUG,
mContext, Settings.Secure.BAR_SERVICE_COMPONENT, this);
mServiceMonitor.start(); // will call onNoService if no remote service is found
}
start中創建ServiceMonitor實例并start();
注釋中說明,/服務沒啟動時,ServiceMonitor會回調SystemBars的onNoService/
所以去看SystemBars的onNoService:
public void onNoService() {
createStatusBarFromConfig(); // fallback to using an in-process implementation
}
直接去看createStatusBarFromConfig():
private void createStatusBarFromConfig() {
final String clsName = mContext.getString(R.string.config_statusBarComponent);
if (clsName == null || clsName.length() == 0) {
throw andLog("No status bar component configured", null);
}
Class<?> cls = null;
try {
cls = mContext.getClassLoader().loadClass(clsName);
} catch (Throwable t) {
throw andLog("Error loading status bar component: " + clsName, t);
}
try {
mStatusBar = (BaseStatusBar) cls.newInstance();
} catch (Throwable t) {
throw andLog("Error creating status bar component: " + clsName, t);
}
mStatusBar.mContext = mContext;
mStatusBar.mComponents = mComponents;
mStatusBar.start();
}
clsName得到的string為com.android.systemui.statusbar.phone.PhoneStatusBar
通過反射機制得到PhoneStatusBar實例:
cls = mContext.getClassLoader().loadClass(clsName);
...
mStatusBar = (BaseStatusBar) cls.newInstance();
并調用start方法:
PhoneStatusBar繼承自BaseStatusBar;
PhoneStatusBar中調用了BaseStatusBar的start()
BaseStatusBar.start():
public void start() {
mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
mWindowManagerService = WindowManagerGlobal.getWindowManagerService();
mDisplay = mWindowManager.getDefaultDisplay();
mDevicePolicyManager = (DevicePolicyManager)mContext.getSystemService(
Context.DEVICE_POLICY_SERVICE);
mNotificationColorUtil = NotificationColorUtil.getInstance(mContext);
mNotificationData = new NotificationData(this);
mAccessibilityManager = (AccessibilityManager)
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
mDreamManager = IDreamManager.Stub.asInterface(
ServiceManager.checkService(DreamService.DREAM_SERVICE));
mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
.......
//在這里實例化了許多systemui常用的對象,服務,Manager,Observer等等
.......
createAndAddWindows(); //本本文而言一個非常重要的方法
查看createAndAddWindows():
protected abstract void createAndAddWindows();
是個抽象方法,很顯然調用去了BaseStatusBar的子類中,即PhoneStatusBar中
PhoneStatusBar的createAndAddWindows():
@Override
public void createAndAddWindows() {
addStatusBarWindow();
}
private void addStatusBarWindow() {
makeStatusBarView();//關鍵方法,創建StatusBarView
mStatusBarWindowManager = new StatusBarWindowManager(mContext);
mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
}
可以看到這里最終調用了makeStatusBarView方法:
protected PhoneStatusBarView makeStatusBarView() {
final Context context = mContext;
Resources res = context.getResources();
updateDisplaySize(); // populates mDisplayMetrics
updateResources();
mStatusBarWindow = (StatusBarWindowView) View.inflate(context,
R.layout.super_status_bar, null);
........
mStatusBarView = (PhoneStatusBarView) mStatusBarWindow.findViewById(R.id.status_bar);
mStatusBarView.setBar(this);
PanelHolder holder = (PanelHolder) mStatusBarWindow.findViewById(R.id.panel_holder);
mStatusBarView.setPanelHolder(holder);
mNotificationPanel = (NotificationPanelView) mStatusBarWindow.findViewById(
R.id.notification_panel);
mNotificationPanel.setStatusBar(this);
if (!ActivityManager.isHighEndGfx()) {
mStatusBarWindow.setBackground(null);
mNotificationPanel.setBackground(new FastColorDrawable(context.getColor(
R.color.notification_panel_solid_background)));
}
........
mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header);
mKeyguardStatusView = mStatusBarWindow.findViewById(R.id.keyguard_status_view);
mKeyguardBottomArea =
(KeyguardBottomAreaView) mStatusBarWindow.findViewById(R.id.keyguard_bottom_area);
........
//可以看到這里完成了許多systemui關鍵組件的view創建。這個方法很重要。
........
// Set up the quick settings tile panel
mQSPanel = (QSPanel) mStatusBarWindow.findViewById(R.id.quick_settings_panel);
if (mQSPanel != null) {
final QSTileHost qsh = new QSTileHost(mContext, this,
mBluetoothController, mLocationController, mRotationLockController,
mNetworkController, mZenModeController, mHotspotController,
mCastController, mFlashlightController,
mUserSwitcherController, mKeyguardMonitor,
mSecurityController,
mAudioProfileController
);
mQSPanel.setHost(qsh);
mQSPanel.setTiles(qsh.getTiles());
mHeader.setQSPanel(mQSPanel);
qsh.setCallback(new QSTileHost.Callback() {
@Override
public void onTilesChanged() {
mQSPanel.setTiles(qsh.getTiles());
}
});
}
.........
看到QSPanel,發現了我們的目標,它是下拉狀態欄的一個關鍵類。
在mQSPanel加載xml布局之后,創建QSTileHost對象。
直接去看QSTileHost的繼承關系及構造方法:
public class QSTileHost implements QSTile.Host, Tunable {
.......
//這里QSTileHost繼承了Tunable接口,下文將有一個非常經典的java回調實現
public QSTileHost(Context context, PhoneStatusBar statusBar,
........
TunerService.get(mContext).addTunable(this, TILES_SETTING);
//回調實現的第一步
//這里傳了this。
//將繼承了Tunable接口的QSTileHost傳遞給TunerService的addTunable();
}
重點即為TunerService的addTunable。TunerService看起來有點眼熟,向上查看文章,它和systembars一起
在SystemUIApplication中被實例化并開啟
來看TunerService的addTunable:
public void addTunable(Tunable tunable, String... keys) {
for (String key : keys) {
addTunable(tunable, key);
}
}
private void addTunable(Tunable tunable, String key) {
if (!mTunableLookup.containsKey(key)) {
mTunableLookup.put(key, new ArrayList<Tunable>());
}
mTunableLookup.get(key).add(tunable);
Uri uri = Settings.Secure.getUriFor(key);
if (!mListeningUris.containsKey(uri)) {
mListeningUris.put(uri, key);
mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
}
// Send the first state.
String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
//這里的value將獲得一個長字符,包含了所有系統QSTitle組件名,在后面進行分割,生成具體的QSTitle對象
//例子: wifi,location,dataconnection,hotspot,audioprofile,bt,rotation,airplane,screenshot
tunable.onTuningChanged(key, value);//回調第二步,addTunable傳遞來的QSTileHost對象調用自己的onTuningChanged方法
}
來看QSTileHost的onTuningChanged()
@Override
public void onTuningChanged(String key, String newValue) {
if (!TILES_SETTING.equals(key)) {
return;
}
final List<String> tileSpecs = loadTileSpecs(newValue);//切割傳遞來的newValue為List
if (tileSpecs.equals(mTileSpecs)) return;
for (Map.Entry<String, QSTile<?>> tile : mTiles.entrySet()) {
if (!tileSpecs.contains(tile.getKey())) {
tile.getValue().destroy();
}
}
final LinkedHashMap<String, QSTile<?>> newTiles = new LinkedHashMap<>();
for (String tileSpec : tileSpecs) {
if (mTiles.containsKey(tileSpec)) {
newTiles.put(tileSpec, mTiles.get(tileSpec));
} else {
if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec);
try {
newTiles.put(tileSpec, createTile(tileSpec));
//createTile()中根據上文切割出的QSTitle名創建相應的對象
} catch (Throwable t) {
}
}
}
mTileSpecs.clear();
mTileSpecs.addAll(tileSpecs);
mTiles.clear();
mTiles.putAll(newTiles);
if (mCallback != null) {
mCallback.onTilesChanged();
}
}
看看createTile(tileSpec)
protected QSTile<?> createTile(String tileSpec) {
if (tileSpec.equals("wifi")) return new WifiTile(this);
else if (tileSpec.equals("bt")) return new BluetoothTile(this);
else if (tileSpec.equals("inversion")) return new ColorInversionTile(this);
else if (tileSpec.equals("cell")) return new CellularTile(this);
else if (tileSpec.equals("airplane")) return new AirplaneModeTile(this);
else if (tileSpec.equals("dnd")) return new DndTile(this);
else if (tileSpec.equals("rotation")) return new RotationLockTile(this);
else if (tileSpec.equals("flashlight")) return new FlashlightTile(this);
else if (tileSpec.equals("location")) return new LocationTile(this);
else if (tileSpec.equals("cast")) return new CastTile(this);
else if (tileSpec.equals("hotspot")) return new HotspotTile(this);
else throw new IllegalArgumentException("Bad tile spec: " + tileSpec);
}
可以看到這里具體創建了QSTitle的各個對象。
那么這些QSTitle對象又是如何加載到QSPanel的View中呢?
而QSPanel在哪加入到了StatusBarHeaderView中。
回頭在看看PhoneStatusBar的makeStatusBarView方法:
// Set up the quick settings tile panel
mQSPanel = (QSPanel) mStatusBarWindow.findViewById(R.id.quick_settings_panel);
if (mQSPanel != null) {
final QSTileHost qsh = new QSTileHost(mContext, this,
mBluetoothController, mLocationController, mRotationLockController,
mNetworkController, mZenModeController, mHotspotController,
mCastController, mFlashlightController,
mUserSwitcherController, mKeyguardMonitor,
mSecurityController,
mAudioProfileController
);
mQSPanel.setHost(qsh);
mQSPanel.setTiles(qsh.getTiles());
mHeader.setQSPanel(mQSPanel);//mHeader->StatusBarHeaderView QSPanel加入到了StatusBarHeaderView中。
qsh.setCallback(new QSTileHost.Callback() {
@Override
public void onTilesChanged() {
mQSPanel.setTiles(qsh.getTiles());
}
});
}
剛才我們從QSTileHost的構造函數開始,分析了具體每個QSTitle的實例化。我們現在創建QSTileHost之后,又發生了什么。
mQSPanel.setHost(qsh);//將QSTileHost對象放入QSPanel
mQSPanel.setTiles(qsh.getTiles());
/*QSTileHost獲得title用來設置QSPanel的title,從函數名來看,
很像是我們上面問題的答案
*/
看看qsh.getTiles()及mQSPanel.setTiles()
@Override
public Collection<QSTile<?>> getTiles() {
return mTiles.values();//獲得title值
}
public void setTiles(Collection<QSTile<?>> tiles) {
for (TileRecord record : mRecords) {
removeView(record.tileView);
}
mRecords.clear();
for (QSTile<?> tile : tiles) {
addTile(tile);//在這里,具體的每個QStitle在這里被addTile進了QSPanel
}
if (isShowingDetail()) {
mDetail.bringToFront();
}
}
看看addTile():
private void addTile(final QSTile<?> tile) {
final TileRecord r = new TileRecord();
r.tile = tile;
r.tileView = tile.createTileView(mContext);
r.tileView.setVisibility(View.GONE);
final QSTile.Callback callback = new QSTile.Callback() {
@Override
public void onStateChanged(QSTile.State state) {
if (!r.openingDetail) {
drawTile(r, state);
}
}
@Override
public void onShowDetail(boolean show) {
QSPanel.this.showDetail(show, r);
}
@Override
public void onToggleStateChanged(boolean state) {
if (mDetailRecord == r) {
fireToggleStateChanged(state);
}
}
@Override
public void onScanStateChanged(boolean state) {
r.scanState = state;
if (mDetailRecord == r) {
fireScanStateChanged(r.scanState);
}
}
@Override
public void onAnnouncementRequested(CharSequence announcement) {
announceForAccessibility(announcement);
}
};
r.tile.setCallback(callback);
final View.OnClickListener click = new View.OnClickListener() {
@Override
public void onClick(View v) {
r.tile.click();
}
};
final View.OnClickListener clickSecondary = new View.OnClickListener() {
@Override
public void onClick(View v) {
r.tile.secondaryClick();
}
};
final View.OnLongClickListener longClick = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
r.tile.longClick();
return true;
}
};
r.tileView.init(click, clickSecondary, longClick);
r.tile.setListening(mListening);
callback.onStateChanged(r.tile.getState());
r.tile.refreshState();
mRecords.add(r);
addView(r.tileView);//加載進QSPanel
}
addTile()中創建了一個callback,實際運行中Title刷新,點擊事件等許多操作都將與這個callback掛鉤
同時,最后addView函數也將這個QSTitle加載進了QSPanel中。
至此,文章告一段落。
參考文章:
Android SystemServer 啟動流程
Android之SystemUI加載流程和NavigationBar的分析
http://www.2cto.com/kf/201604/499625.html
Android 6.0 系統學習之 Zygote
http://www.open-open.com/lib/view/open1449567150379.html