PhoneGap插件调用Java流程源码分析(二)
来源:互联网 发布:python import路径 编辑:程序博客网 时间:2024/06/04 19:56
PhoneGap插件调用Java流程源码分析
回顾一下上一篇 PhoneGap插件调用Java流程源码分析(一)最后的话题,当调用webview.loadUrl(...)会需要判断是否初始化,如果没有就需要调用一下init方法:
private void initIfNecessary() { if (pluginManager == null) { Log.w(TAG, "CordovaWebView.init() was not called. This will soon be required."); // Before the refactor to a two-phase init, the Context needed to implement CordovaInterface. CordovaInterface cdv = (CordovaInterface)getContext(); if (!Config.isInitialized()) { Config.init(cdv.getActivity()); } init(cdv, makeWebViewClient(cdv), makeWebChromeClient(cdv), Config.getPluginEntries(), Config.getWhitelist(), Config.getExternalWhitelist(), Config.getPreferences()); } }
然后调用很多参数的init方法:
public void init(CordovaInterface cordova, CordovaWebViewClient webViewClient, CordovaChromeClient webChromeClient, List<PluginEntry> pluginEntries, Whitelist internalWhitelist, Whitelist externalWhitelist, CordovaPreferences preferences) { if (this.cordova != null) { throw new IllegalStateException(); } this.cordova = cordova; this.viewClient = webViewClient; this.chromeClient = webChromeClient; this.internalWhitelist = internalWhitelist; this.externalWhitelist = externalWhitelist; this.preferences = preferences; super.setWebChromeClient(webChromeClient); super.setWebViewClient(webViewClient); pluginManager = new PluginManager(this, this.cordova, pluginEntries); bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova)); resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); pluginManager.addService("App", "org.apache.cordova.App"); initWebViewSettings(); exposeJsInterface(); }初始化了webViewClient,webChromeClient;
初始化了PluginManager(那么这个是做什么呢?后面分析),并且注册了配置的Plugin;
初始化了一个CordovaBridge(bridge变量);
初始化了webview的设置;
最终和JS接口关联起来,exposeJsInterface();
CordovaChromeClient分析
第一步,上关键源码:public class CordovaChromeClient extends WebChromeClient { public static final int FILECHOOSER_RESULTCODE = 5173; private String TAG = "CordovaLog"; private long MAX_QUOTA = 100 * 1024 * 1024; protected CordovaInterface cordova; protected CordovaWebView appView; // the video progress view private View mVideoProgressView; // File Chooser public ValueCallback<Uri> mUploadMessage; @Deprecated public CordovaChromeClient(CordovaInterface cordova) { this.cordova = cordova; } public CordovaChromeClient(CordovaInterface ctx, CordovaWebView app) { this.cordova = ctx; this.appView = app; } @Deprecated public void setWebView(CordovaWebView view) { this.appView = view; } /** * Tell the client to display a javascript alert dialog. * * @param view * @param url * @param message * @param result * @see Other implementation in the Dialogs plugin. */ @Override public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); dlg.setMessage(message); dlg.setTitle("Alert"); //Don't let alerts break the back button dlg.setCancelable(true); dlg.setPositiveButton(android.R.string.ok, new AlertDialog.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.confirm(); } }); dlg.setOnCancelListener( new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { result.cancel(); } }); dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { //DO NOTHING public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { result.confirm(); return false; } else return true; } }); dlg.show(); return true; } /** * Tell the client to display a confirm dialog to the user. * * @param view * @param url * @param message * @param result * @see Other implementation in the Dialogs plugin. */ @Override public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); dlg.setMessage(message); dlg.setTitle("Confirm"); dlg.setCancelable(true); dlg.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.confirm(); } }); dlg.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { result.cancel(); } }); dlg.setOnCancelListener( new DialogInterface.OnCancelListener() { public void onCancel(DialogInterface dialog) { result.cancel(); } }); dlg.setOnKeyListener(new DialogInterface.OnKeyListener() { //DO NOTHING public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { result.cancel(); return false; } else return true; } }); dlg.show(); return true; } /** * Tell the client to display a prompt dialog to the user. * If the client returns true, WebView will assume that the client will * handle the prompt dialog and call the appropriate JsPromptResult method. * * Since we are hacking prompts for our own purposes, we should not be using them for * this purpose, perhaps we should hack console.log to do this instead! * * @see Other implementation in the Dialogs plugin. */ @Override public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, JsPromptResult result) { // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread. String handledRet = appView.bridge.promptOnJsPrompt(origin, message, defaultValue); if (handledRet != null) { result.confirm(handledRet); } else { // Returning false would also show a dialog, but the default one shows the origin (ugly). final JsPromptResult res = result; AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); dlg.setMessage(message); final EditText input = new EditText(this.cordova.getActivity()); if (defaultValue != null) { input.setText(defaultValue); } dlg.setView(input); dlg.setCancelable(false); dlg.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { String usertext = input.getText().toString(); res.confirm(usertext); } }); dlg.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { res.cancel(); } }); dlg.show(); } return true; } ...............}继承自 WebChromClient 的类,重写了其中的 onJsAlert,onJsConfirm,onJsPrompt 等方法。
WebChromClient 原来这三个onJsXX方法的作用是:实现JavaScript中的警示对话框、确认对话框和提示对话框。
查看onJsAlert,onJsConfirm的代码块,其实就是把对话框用android代码实现,而不是使用js的对话框。
而在 onJsPrompt() 方法中,在把对话框实现之前,做了一堆操作。因此,值得我们专门的关注其代码:
/** * Tell the client to display a prompt dialog to the user. * If the client returns true, WebView will assume that the client will * handle the prompt dialog and call the appropriate JsPromptResult method. * * Since we are hacking prompts for our own purposes, we should not be using them for * this purpose, perhaps we should hack console.log to do this instead! * * @see Other implementation in the Dialogs plugin. */ @Override public boolean onJsPrompt(WebView view, String origin, String message, String defaultValue, JsPromptResult result) { // Unlike the @JavascriptInterface bridge, this method is always called on the UI thread. String handledRet = appView.bridge.promptOnJsPrompt(origin, message, defaultValue); if (handledRet != null) { result.confirm(handledRet); } else { // Returning false would also show a dialog, but the default one shows the origin (ugly). final JsPromptResult res = result; AlertDialog.Builder dlg = new AlertDialog.Builder(this.cordova.getActivity()); dlg.setMessage(message); final EditText input = new EditText(this.cordova.getActivity()); if (defaultValue != null) { input.setText(defaultValue); } dlg.setView(input); dlg.setCancelable(false); dlg.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { String usertext = input.getText().toString(); res.confirm(usertext); } }); dlg.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { res.cancel(); } }); dlg.show(); } return true; }看来,这里实现了 PhoneGap 中 Java 端和浏览器端通讯的关键一步:实现 JavaScript 与 Java 端通讯的原理是 JavaScript 利用 prompt 来传递调用信息的数据,在 onJsPrompt 中,重写的方法截获了这些数据,调用:
String handledRet = appView.bridge.promptOnJsPrompt(origin, message, defaultValue);这里调用了appview(cordovawebview)中初始化的CordovaBridge的promptOnJsPrompt方法,进去看看做了什么事情:
public String promptOnJsPrompt(String origin, String message, String defaultValue) { if (defaultValue != null && defaultValue.length() > 3 && defaultValue.startsWith("gap:")) { JSONArray array; try { array = new JSONArray(defaultValue.substring(4)); int bridgeSecret = array.getInt(0); String service = array.getString(1); String action = array.getString(2); String callbackId = array.getString(3); String r = jsExec(bridgeSecret, service, action, callbackId, message); return r == null ? "" : r; } catch (JSONException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return ""; } // Sets the native->JS bridge mode. else if (defaultValue != null && defaultValue.startsWith("gap_bridge_mode:")) { try { int bridgeSecret = Integer.parseInt(defaultValue.substring(16)); jsSetNativeToJsBridgeMode(bridgeSecret, Integer.parseInt(message)); } catch (NumberFormatException e){ e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return ""; } // Polling for JavaScript messages else if (defaultValue != null && defaultValue.startsWith("gap_poll:")) { int bridgeSecret = Integer.parseInt(defaultValue.substring(9)); try { String r = jsRetrieveJsMessages(bridgeSecret, "1".equals(message)); return r == null ? "" : r; } catch (IllegalAccessException e) { e.printStackTrace(); } return ""; } else if (defaultValue != null && defaultValue.startsWith("gap_init:")) { // Protect against random iframes being able to talk through the bridge. // Trust only file URLs and the start URL's domain. // The extra origin.startsWith("http") is to protect against iframes with data: having "" as origin. if (origin.startsWith("file:") || (origin.startsWith("http") && loadedUrl.startsWith(origin))) { // Enable the bridge int bridgeMode = Integer.parseInt(defaultValue.substring(9)); jsMessageQueue.setBridgeMode(bridgeMode); // Tell JS the bridge secret. int secret = generateBridgeSecret(); return ""+secret; } else { Log.e(LOG_TAG, "gap_init called from restricted origin: " + origin); } return ""; } return null; }完成了对数据格式等等分析后,按照要求进行具体的调用:
String r = jsExec(bridgeSecret, service, action, callbackId, message);
public String jsExec(int bridgeSecret, String service, String action, String callbackId, String arguments) throws JSONException, IllegalAccessException { if (!verifySecret("exec()", bridgeSecret)) { return null; } // If the arguments weren't received, send a message back to JS. It will switch bridge modes and try again. See CB-2666. // We send a message meant specifically for this case. It starts with "@" so no other message can be encoded into the same string. if (arguments == null) { return "@Null arguments."; } jsMessageQueue.setPaused(true); try { // Tell the resourceApi what thread the JS is running on. CordovaResourceApi.jsThread = Thread.currentThread(); pluginManager.exec(service, action, callbackId, arguments); String ret = null; if (!NativeToJsMessageQueue.DISABLE_EXEC_CHAINING) { ret = jsMessageQueue.popAndEncode(false); } return ret; } catch (Throwable e) { e.printStackTrace(); return ""; } finally { jsMessageQueue.setPaused(false); } }看关键代码:
pluginManager.exec(service, action, callbackId, arguments);这里就调用了pluginManager去调用具体的plugin(记得初始化的时候会注册配置文件里面的plugin,然后根据名字来查找调用).
CordovaWebView 分析
再次回到CordovaWebView里面看看,到底JS是如何和我们Java端关联起来的?public void init(CordovaInterface cordova, CordovaWebViewClient webViewClient, CordovaChromeClient webChromeClient, List<PluginEntry> pluginEntries, Whitelist internalWhitelist, Whitelist externalWhitelist, CordovaPreferences preferences) { if (this.cordova != null) { throw new IllegalStateException(); } this.cordova = cordova; this.viewClient = webViewClient; this.chromeClient = webChromeClient; this.internalWhitelist = internalWhitelist; this.externalWhitelist = externalWhitelist; this.preferences = preferences; super.setWebChromeClient(webChromeClient); super.setWebViewClient(webViewClient); pluginManager = new PluginManager(this, this.cordova, pluginEntries); bridge = new CordovaBridge(pluginManager, new NativeToJsMessageQueue(this, cordova)); resourceApi = new CordovaResourceApi(this.getContext(), pluginManager); pluginManager.addService("App", "org.apache.cordova.App"); initWebViewSettings(); exposeJsInterface(); }对WebSettings做一些设置,包括:启用js,设置数据库,设置缓存。
private void initWebViewSettings() { this.setInitialScale(0); this.setVerticalScrollBarEnabled(false); // TODO: The Activity is the one that should call requestFocus(). if (shouldRequestFocusOnInit()) {this.requestFocusFromTouch();}// Enable JavaScript WebSettings settings = this.getSettings(); settings.setJavaScriptEnabled(true);//关键点,使能启动JS,能够执行JS脚本 settings.setJavaScriptCanOpenWindowsAutomatically(true);//使能Js自动打开窗口 settings.setLayoutAlgorithm(LayoutAlgorithm.NORMAL); // Set the nav dump for HTC 2.x devices (disabling for ICS, deprecated entirely for Jellybean 4.2) try { Method gingerbread_getMethod = WebSettings.class.getMethod("setNavDump", new Class[] { boolean.class }); String manufacturer = Build.MANUFACTURER; Log.d(TAG, "CordovaWebView is running on device made by: " + manufacturer); if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB && Build.MANUFACTURER.contains("HTC")) { gingerbread_getMethod.invoke(settings, true); } } catch (NoSuchMethodException e) { Log.d(TAG, "We are on a modern version of Android, we will deprecate HTC 2.3 devices in 2.8"); } catch (IllegalArgumentException e) { Log.d(TAG, "Doing the NavDump failed with bad arguments"); } catch (IllegalAccessException e) { Log.d(TAG, "This should never happen: IllegalAccessException means this isn't Android anymore"); } catch (InvocationTargetException e) { Log.d(TAG, "This should never happen: InvocationTargetException means this isn't Android anymore."); } //We don't save any form data in the application settings.setSaveFormData(false); settings.setSavePassword(false); // Jellybean rightfully tried to lock this down. Too bad they didn't give us a whitelist // while we do this if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) Level16Apis.enableUniversalAccess(settings); // Enable database // We keep this disabled because we use or shim to get around DOM_EXCEPTION_ERROR_16 String databasePath = getContext().getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath(); settings.setDatabaseEnabled(true); settings.setDatabasePath(databasePath); //Determine whether we're in debug or release mode, and turn on Debugging! ApplicationInfo appInfo = getContext().getApplicationContext().getApplicationInfo(); if ((appInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { enableRemoteDebugging(); } settings.setGeolocationDatabasePath(databasePath); // Enable DOM storage settings.setDomStorageEnabled(true); // Enable built-in geolocation settings.setGeolocationEnabled(true); // Enable AppCache // Fix for CB-2282 settings.setAppCacheMaxSize(5 * 1048576); settings.setAppCachePath(databasePath); settings.setAppCacheEnabled(true); // Fix for CB-1405 // Google issue 4641 settings.getUserAgentString(); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); if (this.receiver == null) { this.receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { getSettings().getUserAgentString(); } }; getContext().registerReceiver(this.receiver, intentFilter); } // end CB-1405 }再来看看exposeJsInterface()函数:
private void exposeJsInterface() { if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1)) { Log.i(TAG, "Disabled addJavascriptInterface() bridge since Android version is old."); // Bug being that Java Strings do not get converted to JS strings automatically. // This isn't hard to work-around on the JS side, but it's easier to just // use the prompt bridge instead. return; } this.addJavascriptInterface(new ExposedJsApi(bridge), "_cordovaNative"); }把exposedJsApi作为JS的接口添加到webView中,名字为“_cordovaNative”。JS端就是通过这个名字来调用Java端函数。
之前在CordovaChromeClient中调用onJsPrompt,之后就会调用promtOnJsPrompt,然后就调用CordovaBridge的jsExec方法,然后调用到pluginManger.exec(service, action, callbackId, arguments),那么去看看这里面具体做了什么?
/** * Receives a request for execution and fulfills it by finding the appropriate * Java class and calling it's execute method. * * PluginManager.exec can be used either synchronously or async. In either case, a JSON encoded * string is returned that will indicate if any errors have occurred when trying to find * or execute the class denoted by the clazz argument. * * @param service String containing the service to run * @param action String containing the action that the class is supposed to perform. This is * passed to the plugin execute method and it is up to the plugin developer * how to deal with it. * @param callbackId String containing the id of the callback that is execute in JavaScript if * this is an async plugin call. * @param rawArgs An Array literal string containing any arguments needed in the * plugin execute method. */ public void exec(final String service, final String action, final String callbackId, final String rawArgs) { CordovaPlugin plugin = getPlugin(service); if (plugin == null) { Log.d(TAG, "exec() call to unknown plugin: " + service); PluginResult cr = new PluginResult(PluginResult.Status.CLASS_NOT_FOUND_EXCEPTION); app.sendPluginResult(cr, callbackId); return; } CallbackContext callbackContext = new CallbackContext(callbackId, app); try { long pluginStartTime = System.currentTimeMillis(); boolean wasValidAction = plugin.execute(action, rawArgs, callbackContext); long duration = System.currentTimeMillis() - pluginStartTime; if (duration > SLOW_EXEC_WARNING_THRESHOLD) { Log.w(TAG, "THREAD WARNING: exec() call to " + service + "." + action + " blocked the main thread for " + duration + "ms. Plugin should use CordovaInterface.getThreadPool()."); } if (!wasValidAction) { PluginResult cr = new PluginResult(PluginResult.Status.INVALID_ACTION); callbackContext.sendPluginResult(cr); } } catch (JSONException e) { PluginResult cr = new PluginResult(PluginResult.Status.JSON_EXCEPTION); callbackContext.sendPluginResult(cr); } catch (Exception e) { Log.e(TAG, "Uncaught exception from plugin", e); callbackContext.error(e.getMessage()); } }从代码看,通过service名字获取一个plugin(里面怎么获取到的先不管),创建一个CallbackContext对象,然后调用具体的plugin.execute(其实我们利用cordova开发更多的是通过继承plugin来开发我们自己的插件).根据执行的返回结果设置PluginResult.
第一步:获得plugin
CordovaPlugin plugin = getPlugin(service);
/** * Get the plugin object that implements the service. * If the plugin object does not already exist, then create it. * If the service doesn't exist, then return null. * * @param service The name of the service. * @return CordovaPlugin or null */ public CordovaPlugin getPlugin(String service) { CordovaPlugin ret = pluginMap.get(service); if (ret == null) { PluginEntry pe = entryMap.get(service); if (pe == null) { return null; } if (pe.plugin != null) { ret = pe.plugin; } else { ret = instantiatePlugin(pe.pluginClass); } ret.privateInitialize(ctx, app, app.getPreferences()); pluginMap.put(service, ret); } return ret; }首先从pluginMap中取,如果不存在就从entryMap中去取,取不到就返回,取到就实例化一下,并且初始化一下,并放入pluginMap中。那么我们就一个一个来分析:
1)pluginMap,entryMap在哪里初始化?关键点:看看cordovaWebview init时对pluginManager的初始化
private final HashMap<String, PluginEntry> entryMap = new HashMap<String, PluginEntry>();
private final HashMap<String, CordovaPlugin> pluginMap = new HashMap<String, CordovaPlugin>();
PluginManager(CordovaWebView cordovaWebView, CordovaInterface cordova, List<PluginEntry> pluginEntries) { this.ctx = cordova; this.app = cordovaWebView; if (pluginEntries == null) { ConfigXmlParser parser = new ConfigXmlParser(); parser.parse(ctx.getActivity()); pluginEntries = parser.getPluginEntries(); } setPluginEntries(pluginEntries); }
public void setPluginEntries(List<PluginEntry> pluginEntries) { this.onPause(false); this.onDestroy(); pluginMap.clear(); urlMap.clear(); for (PluginEntry entry : pluginEntries) { addService(entry); } }
public void addService(PluginEntry entry) { this.entryMap.put(entry.service, entry); List<String> urlFilters = entry.getUrlFilters(); if (urlFilters != null) { urlMap.put(entry.service, urlFilters); } if (entry.plugin != null) { entry.plugin.privateInitialize(ctx, app, app.getPreferences()); pluginMap.put(entry.service, entry.plugin); } }从这里就可以看到,所有plugin信息都会存入entryMap,只有经过了初始化的plugin才会放入到pluginMap。
那到底是怎么初始化plugin的呢?
public final void privateInitialize(CordovaInterface cordova, CordovaWebView webView, CordovaPreferences preferences) { assert this.cordova == null; this.cordova = cordova; this.webView = webView; this.preferences = preferences; initialize(cordova, webView); pluginInitialize(); }在cordovaPlugin中,initialize和pluginInitialize都是空实现,我们可以在我们自定义的plugin中覆写.
到此plugin初始化完毕.
2)我们自定义的plugin一般写法?
public class xxxextends CordovaPlugin { /** * Constructor. */ public xxx() { } /** * Sets the context of the Command. This can then be used to do things like * get file paths associated with the Activity. * * @param cordova The context of the main Activity. * @param webView The CordovaWebView Cordova is running in. */ public void initialize(CordovaInterface cordova, CordovaWebView webView) { super.initialize(cordova, webView); ....... } /** * Executes the request and returns PluginResult. * * @param action The action to execute. * @param args JSONArry of arguments for the plugin. * @param callbackContext The callback id used when calling back into JavaScript. * @return True if the action was valid, false if not. */ public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException { if (action.equals("getDeviceInfo")) { JSONObject r = new JSONObject(); //省略实现 callbackContext.success(r);//回调结果,表示正常处理 } else { return false; } return true; }}3)上面说到,当pluginMap中没有就需要调用entryMap去取然后实例化,那么怎么实例化plugin?
/** * Create a plugin based on class name. */ private CordovaPlugin instantiatePlugin(String className) { CordovaPlugin ret = null; try { Class<?> c = null; if ((className != null) && !("".equals(className))) { c = Class.forName(className); } if (c != null & CordovaPlugin.class.isAssignableFrom(c)) { ret = (CordovaPlugin) c.newInstance(); } } catch (Exception e) { e.printStackTrace(); System.out.println("Error adding plugin " + className + "."); } return ret; }其实就是根据Java的反射机制,通过className来获得class(Class.forName),然后调用class.newInstance()方法。
4)当我们plugin处理完就会返回一个成功的标志给webview.
至此:整个js调用本地的过程已经清楚了:
1. 在CordovaChromeClient的onJsPrompt方法调用ExposedJsApi的exec方法。
2. ExposedJsApi的exec,通过找到的plugin调用java端的execute方法并返回。
3. 返回结果通知给CordovaWebView。
- PhoneGap插件调用Java流程源码分析(二)
- PhoneGap插件调用Java流程源码分析(一)
- PhoneGap插件调用Java流程源码分析(三)
- PhoneGap插件调用Java流程源码分析(四)
- phonegap源码分析(二)------ Windows Phone
- phonegap源码分析(二)------ Windows Phone
- PhoneGap插件开发流程
- vlc源码分析(二) 播放流程
- vlc源码分析(二) 播放流程
- iOS phoneGap的使用(二、自定义phoneGap插件)
- Java 源码分析(二)
- Service源码分析系列(二):bindService流程分析
- phonegap源码分析(一)------ android
- phonegap源码分析(三)------ IOS
- phonegap源码分析(一)------ android
- phonegap源码分析(一)------ android
- phonegap源码分析(三)------ IOS
- phonegap源码分析(一)------ android
- 初步探究ES6之解构
- NSXMLParser解析XML数据
- 工作周报067
- JavaScript DOM编程艺术—javascript实现移动元素动画
- my_zshrc
- PhoneGap插件调用Java流程源码分析(二)
- 代理模式(Proxy Pattern)
- excel to DataSet
- Python基础——拾遗
- Linux终端打印的常用命令echo和printf
- House Robber
- java对mongodb的基础操作(1)
- 乔布斯信条
- Android实现多层级Spinner列表选项实时更新树形层级