Android OpenGL ES 2.0学习研究 (一)
来源:互联网 发布:ubuntu解压rar文件 编辑:程序博客网 时间:2024/06/02 12:47
Android OpenGL ES 2.0学习研究 (一)
基于对 Google 的 Gallery 代码的研究和修改,对 OpenGL ES 2.0 在 Android 中的使用进行总结;
这一篇主要集中于四点进行简要介绍:GLRootView(base) + GLView(UI) + GLES20Canvas(canvas) + Texture;
关于OpenGL ES的基础知识可以参考:OpenGL ES 简明教程
概述:
目前,关于GLES的教程也有不少,但是基本上都是基础知识介绍,很零散;
Google官方的Gallery则对代码进行了封装,结构更加清晰灵活,非常适合学习研究GLES;
GLRootView(base)+ GLES20Canvas(canvas) + GLView(ui) + Texture:
GLRootView是一个GLSurfaceView,通过GLSurfaceView.Renderer的三大方法将GLView绘制到GLCanvas上:
- GLSurfaceView:
- 起到连接 OpenGL ES与Android 的 View 层次结构之间的桥梁作用。
- 使得 Open GL ES 库适应于 Anndroid 系统的 Activity 生命周期。
- 使得选择合适的 Frame buffer 像素格式变得容易。
- 创建和管理单独绘图线程以达到平滑动画效果。
- 提供了方便使用的调试工具来跟踪 OpenGL ES 函数调用以帮助检查错误。
public void setRenderer(GLSurfaceView.Renderer renderer)
GLSurfaceView.Renderer:
public void onSurfaceCreated(GL10 gl, EGLConfig config)
public void onDrawFrame(GL10 gl)
public void onSurfaceChanged(GL10 gl, int width, int height)
GLES20Canvas相当于Canvas,设置好Vertex shader和Fragment shader等参数,将GLView绘制出来:
- GLES的使用都集中在这里处理,相当于从GLSurfaceView中抽离出来GLES的代码进行封装处理;
- GLSurfaceView:
GLView是要绘制的UI,可以同时有多个,它处理触摸事件,通过GLRootView将其绘制在GLCanvas上:
- GLView是整个UI的布局;
- 可以在GLView基础上进行继承,充分自定义;
Texture则是绘制的画面,在GLES20Canvas上绘制出来:
- 可以将其细分为ColorTexture、StringTexture、BitmapTexture分别绘制色块、字符串和图片;
接下来,看代码,具体说明看备注:
自定义一个GLRootView,继承GLSurfaceView并实现GLSurfaceView.Renderer接口(GLRoot为自定义接口)
//GLRoot为自定义接口public class GLRootView extends GLSurfaceView implements GLSurfaceView.Renderer, GLRoot { public GLRootView(Context context) { this(context, null); } public GLRootView(Context context, AttributeSet attrs) { super(context, attrs); //进行初始化设置 setBackgroundDrawable(null); setEGLContextClientVersion(2); setEGLConfigChooser(mEglConfigChooser); setRenderer(this); if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { getHolder().setFormat(PixelFormat.RGB_888); } else { getHolder().setFormat(PixelFormat.RGB_565); } } @Override public void onSurfaceCreated(GL10 gl1, EGLConfig config) { GL11 gl = (GL11) gl1; mRenderLock.lock(); try { mGL = gl; //创建GLES20Canvas,创建的时候会自动加载shader mCanvas = new GLES20Canvas(); BasicTexture.invalidateAllTextures(); } finally { mRenderLock.unlock(); } //设置渲染模式为 RENDERMODE_WHEN_DIRTY 以节约性能 setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); //setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); } @Override public void onSurfaceChanged(GL10 gl1, int width, int height) { //设置线程 Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY); GalleryUtils.setRenderThread(); GL11 gl = (GL11) gl1; Utils.assertTrue(mGL == gl); //重新设置GLES20Canvas的尺寸 mCanvas.setSize(width, height); } @Override public void onDrawFrame(GL10 gl) { AnimationTime.update(); mRenderLock.lock(); try { //重点在这,具体的绘制操作 } finally { mRenderLock.unlock(); } //第一次绘制的时候放置一个黑色的背景,以防透明 if (mFirstDraw) { mFirstDraw = false; post(new Runnable() { @Override public void run() { View root = getRootView(); View cover = root.findViewById(R.id.gl_root_cover); cover.setVisibility(GONE); } }); } } //处理触摸事件 @Override public boolean dispatchTouchEvent(MotionEvent event) { if (!isEnabled()) return false; int action = event.getAction(); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mInDownState = false; } else if (!mInDownState && action != MotionEvent.ACTION_DOWN) { return false; } mRenderLock.lock(); try { // If this has been detached from root, we don't need to handle event boolean handled = mContentView != null && mContentView.dispatchTouchEvent(event); if (action == MotionEvent.ACTION_DOWN && handled) { mInDownState = true; } return handled; } finally { mRenderLock.unlock(); } }}
自定义GLES20Canvas
@SuppressLint("NewApi")public class GLES20Canvas implements GLCanvas { private abstract static class ShaderParameter { public int handle; protected final String mName; public ShaderParameter(String name) { mName = name; } public abstract void loadHandle(int program); } //处理shader private static class UniformShaderParameter extends ShaderParameter { public UniformShaderParameter(String name) { super(name); } @Override public void loadHandle(int program) { handle = GLES20.glGetUniformLocation(program, mName); checkError(); } } private static class AttributeShaderParameter extends ShaderParameter { public AttributeShaderParameter(String name) { super(name); } @Override public void loadHandle(int program) { handle = GLES20.glGetAttribLocation(program, mName); checkError(); } } //初始化 public GLES20Canvas() { Matrix.setIdentityM(mTempTextureMatrix, 0); Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); FloatBuffer boxBuffer = createBuffer(BOX_COORDINATES); mBoxCoordinates = uploadBuffer(boxBuffer); //创建的时候就加载shader int drawVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DRAW_VERTEX_SHADER); int textureVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, TEXTURE_VERTEX_SHADER); int meshVertexShader = loadShader(GLES20.GL_VERTEX_SHADER, MESH_VERTEX_SHADER); int drawFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DRAW_FRAGMENT_SHADER); int textureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, TEXTURE_FRAGMENT_SHADER); int oesTextureFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, OES_TEXTURE_FRAGMENT_SHADER); mDrawProgram = assembleProgram(drawVertexShader, drawFragmentShader, mDrawParameters); mTextureProgram = assembleProgram(textureVertexShader, textureFragmentShader, mTextureParameters); mOesTextureProgram = assembleProgram(textureVertexShader, oesTextureFragmentShader, mOesTextureParameters); mMeshProgram = assembleProgram(meshVertexShader, textureFragmentShader, mMeshParameters); GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); checkError(); } @Override public void setSize(int width, int height) { mWidth = width; mHeight = height; GLES20.glViewport(0, 0, mWidth, mHeight); checkError(); Matrix.setIdentityM(mMatrices, mCurrentMatrixIndex); Matrix.orthoM(mProjectionMatrix, 0, 0, width, 0, height, -1, 1); if (getTargetTexture() == null) { mScreenWidth = width; mScreenHeight = height; Matrix.translateM(mMatrices, mCurrentMatrixIndex, 0, height, 0); Matrix.scaleM(mMatrices, mCurrentMatrixIndex, 1, -1, 1); } } private void draw(int type, int offset, int count, float x, float y, float width, float height, int color, float lineWidth) { prepareDraw(offset, color, lineWidth); draw(mDrawParameters, type, count, x, y, width, height); } //设置背景颜色 private void prepareDraw(int offset, int color, float lineWidth) { GLES20.glUseProgram(mDrawProgram); checkError(); if (lineWidth > 0) { GLES20.glLineWidth(lineWidth); checkError(); } float[] colorArray = getColor(color); boolean blendingEnabled = (colorArray[3] < 1f); enableBlending(blendingEnabled); if (blendingEnabled) { GLES20.glBlendColor(colorArray[0], colorArray[1], colorArray[2], colorArray[3]); checkError(); } GLES20.glUniform4fv(mDrawParameters[INDEX_COLOR].handle, 1, colorArray, 0); setPosition(mDrawParameters, offset); checkError(); } //绘制 private void draw(ShaderParameter[] params, int type, int count, float x, float y, float width, float height) { setMatrix(params, x, y, width, height); int positionHandle = params[INDEX_POSITION].handle; GLES20.glEnableVertexAttribArray(positionHandle); checkError(); GLES20.glDrawArrays(type, 0, count); checkError(); GLES20.glDisableVertexAttribArray(positionHandle); checkError(); } public static void checkError() { int error = GLES20.glGetError(); if (error != 0) { Throwable t = new Throwable(); Log.e(TAG, "GL error: " + error, t); } } //以下几个方法均为BitmapTexture调用 @Override public void setTextureParameters(BasicTexture texture) { int target = texture.getTarget(); GLES20.glBindTexture(target, texture.getId()); checkError(); GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(target, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameterf(target, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); } @Override public void initializeTextureSize(BasicTexture texture, int format, int type) { int target = texture.getTarget(); GLES20.glBindTexture(target, texture.getId()); checkError(); int width = texture.getTextureWidth(); int height = texture.getTextureHeight(); GLES20.glTexImage2D(target, 0, format, width, height, 0, format, type, null); } @Override public void initializeTexture(BasicTexture texture, Bitmap bitmap) { int target = texture.getTarget(); GLES20.glBindTexture(target, texture.getId()); checkError(); GLUtils.texImage2D(target, 0, bitmap, 0); } @Override public void texSubImage2D(BasicTexture texture, int xOffset, int yOffset, Bitmap bitmap, int format, int type) { int target = texture.getTarget(); GLES20.glBindTexture(target, texture.getId()); checkError(); GLUtils.texSubImage2D(target, 0, xOffset, yOffset, bitmap, format, type); }}
自定义GLView
public class GLView { public void startAnimation(CanvasAnimation animation) { GLRoot root = getGLRoot(); if (root == null) throw new IllegalStateException(); mAnimation = animation; if (mAnimation != null) { mAnimation.start(); root.registerLaunchedAnimation(mAnimation); } invalidate(); } // Sets the visiblity of this GLView (either GLView.VISIBLE or // GLView.INVISIBLE). public void setVisibility(int visibility) { if (visibility == getVisibility()) return; if (visibility == VISIBLE) { mViewFlags &= ~FLAG_INVISIBLE; } else { mViewFlags |= FLAG_INVISIBLE; } onVisibilityChanged(visibility); invalidate(); } // Returns GLView.VISIBLE or GLView.INVISIBLE public int getVisibility() { return (mViewFlags & FLAG_INVISIBLE) == 0 ? VISIBLE : INVISIBLE; } // This should only be called on the content pane (the topmost GLView). public void attachToRoot(GLRoot root) { Utils.assertTrue(mParent == null && mRoot == null); onAttachToRoot(root); } // This should only be called on the content pane (the topmost GLView). public void detachFromRoot() { Utils.assertTrue(mParent == null && mRoot != null); onDetachFromRoot(); } // Returns the number of children of the GLView. public int getComponentCount() { return mComponents == null ? 0 : mComponents.size(); } // Returns the children for the given index. public GLView getComponent(int index) { if (mComponents == null) { throw new ArrayIndexOutOfBoundsException(index); } return mComponents.get(index); } // Adds a child to this GLView. public void addComponent(GLView component) { // Make sure the component doesn't have a parent currently. if (component.mParent != null) throw new IllegalStateException(); // Build parent-child links if (mComponents == null) { mComponents = new ArrayList<GLView>(); } mComponents.add(component); component.mParent = this; // If this is added after we have a root, tell the component. if (mRoot != null) { component.onAttachToRoot(mRoot); } } // Removes a child from this GLView. public boolean removeComponent(GLView component) { if (mComponents == null) return false; if (mComponents.remove(component)) { removeOneComponent(component); return true; } return false; } // Removes all children of this GLView. public void removeAllComponents() { for (int i = 0, n = mComponents.size(); i < n; ++i) { removeOneComponent(mComponents.get(i)); } mComponents.clear(); } private void removeOneComponent(GLView component) { if (mMotionTarget == component) { long now = SystemClock.uptimeMillis(); MotionEvent cancelEvent = MotionEvent.obtain( now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0); dispatchTouchEvent(cancelEvent); cancelEvent.recycle(); } component.onDetachFromRoot(); component.mParent = null; } public Rect bounds() { return mBounds; } public int getWidth() { return mBounds.right - mBounds.left; } public int getHeight() { return mBounds.bottom - mBounds.top; } public GLRoot getGLRoot() { return mRoot; } // Request re-rendering of the view hierarchy. // This is used for animation or when the contents changed. public void invalidate() { GLRoot root = getGLRoot(); if (root != null) root.requestRender(); } // Request re-layout of the view hierarchy. public void requestLayout() { mViewFlags |= FLAG_LAYOUT_REQUESTED; mLastHeightSpec = -1; mLastWidthSpec = -1; if (mParent != null) { mParent.requestLayout(); } else { // Is this a content pane ? GLRoot root = getGLRoot(); if (root != null) root.requestLayoutContentPane(); } } protected void render(GLCanvas canvas) { boolean transitionActive = false; if (mTransition != null && mTransition.calculate(AnimationTime.get())) { invalidate(); transitionActive = mTransition.isActive(); } renderBackground(canvas); canvas.save(); if (transitionActive) { mTransition.applyContentTransform(this, canvas); } for (int i = 0, n = getComponentCount(); i < n; ++i) { renderChild(canvas, getComponent(i)); } canvas.restore(); if (transitionActive) { mTransition.applyOverlay(this, canvas); } } public void setIntroAnimation(StateTransitionAnimation intro) { mTransition = intro; if (mTransition != null) mTransition.start(); } public float [] getBackgroundColor() { return mBackgroundColor; } public void setBackgroundColor(float [] color) { mBackgroundColor = color; } protected void renderBackground(GLCanvas view) { if (mBackgroundColor != null) { view.clearBuffer(mBackgroundColor); } if (mTransition != null && mTransition.isActive()) { mTransition.applyBackground(this, view); return; } } protected void renderChild(GLCanvas canvas, GLView component) { if (component.getVisibility() != GLView.VISIBLE && component.mAnimation == null) return; int xoffset = component.mBounds.left - mScrollX; int yoffset = component.mBounds.top - mScrollY; canvas.translate(xoffset, yoffset); CanvasAnimation anim = component.mAnimation; if (anim != null) { canvas.save(anim.getCanvasSaveFlags()); if (anim.calculate(AnimationTime.get())) { invalidate(); } else { component.mAnimation = null; } anim.apply(canvas); } component.render(canvas); if (anim != null) canvas.restore(); canvas.translate(-xoffset, -yoffset); } protected boolean onTouch(MotionEvent event) { return false; } protected boolean dispatchTouchEvent(MotionEvent event, int x, int y, GLView component, boolean checkBounds) { Rect rect = component.mBounds; int left = rect.left; int top = rect.top; if (!checkBounds || rect.contains(x, y)) { event.offsetLocation(-left, -top); if (component.dispatchTouchEvent(event)) { event.offsetLocation(left, top); return true; } event.offsetLocation(left, top); } return false; } protected boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); int action = event.getAction(); if (mMotionTarget != null) { if (action == MotionEvent.ACTION_DOWN) { MotionEvent cancel = MotionEvent.obtain(event); cancel.setAction(MotionEvent.ACTION_CANCEL); dispatchTouchEvent(cancel, x, y, mMotionTarget, false); mMotionTarget = null; } else { dispatchTouchEvent(event, x, y, mMotionTarget, false); if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { mMotionTarget = null; } return true; } } if (action == MotionEvent.ACTION_DOWN) { // in the reverse rendering order for (int i = getComponentCount() - 1; i >= 0; --i) { GLView component = getComponent(i); if (component.getVisibility() != GLView.VISIBLE) continue; if (dispatchTouchEvent(event, x, y, component, true)) { mMotionTarget = component; return true; } } } return onTouch(event); } public Rect getPaddings() { return mPaddings; } public void layout(int left, int top, int right, int bottom) { boolean sizeChanged = setBounds(left, top, right, bottom); mViewFlags &= ~FLAG_LAYOUT_REQUESTED; // We call onLayout no matter sizeChanged is true or not because the // orientation may change without changing the size of the View (for // example, rotate the device by 180 degrees), and we want to handle // orientation change in onLayout. onLayout(sizeChanged, left, top, right, bottom); } private boolean setBounds(int left, int top, int right, int bottom) { boolean sizeChanged = (right - left) != (mBounds.right - mBounds.left) || (bottom - top) != (mBounds.bottom - mBounds.top); mBounds.set(left, top, right, bottom); return sizeChanged; } public void measure(int widthSpec, int heightSpec) { if (widthSpec == mLastWidthSpec && heightSpec == mLastHeightSpec && (mViewFlags & FLAG_LAYOUT_REQUESTED) == 0) { return; } mLastWidthSpec = widthSpec; mLastHeightSpec = heightSpec; mViewFlags &= ~FLAG_SET_MEASURED_SIZE; onMeasure(widthSpec, heightSpec); if ((mViewFlags & FLAG_SET_MEASURED_SIZE) == 0) { throw new IllegalStateException(getClass().getName() + " should call setMeasuredSize() in onMeasure()"); } } protected void onMeasure(int widthSpec, int heightSpec) { } protected void setMeasuredSize(int width, int height) { mViewFlags |= FLAG_SET_MEASURED_SIZE; mMeasuredWidth = width; mMeasuredHeight = height; } public int getMeasuredWidth() { return mMeasuredWidth; } public int getMeasuredHeight() { return mMeasuredHeight; } protected void onLayout( boolean changeSize, int left, int top, int right, int bottom) { } /** * Gets the bounds of the given descendant that relative to this view. */ public boolean getBoundsOf(GLView descendant, Rect out) { int xoffset = 0; int yoffset = 0; GLView view = descendant; while (view != this) { if (view == null) return false; Rect bounds = view.mBounds; xoffset += bounds.left; yoffset += bounds.top; view = view.mParent; } out.set(xoffset, yoffset, xoffset + descendant.getWidth(), yoffset + descendant.getHeight()); return true; } protected void onVisibilityChanged(int visibility) { for (int i = 0, n = getComponentCount(); i < n; ++i) { GLView child = getComponent(i); if (child.getVisibility() == GLView.VISIBLE) { child.onVisibilityChanged(visibility); } } } protected void onAttachToRoot(GLRoot root) { mRoot = root; for (int i = 0, n = getComponentCount(); i < n; ++i) { getComponent(i).onAttachToRoot(root); } } protected void onDetachFromRoot() { for (int i = 0, n = getComponentCount(); i < n; ++i) { getComponent(i).onDetachFromRoot(); } mRoot = null; } public void lockRendering() { if (mRoot != null) { mRoot.lockRenderThread(); } } public void unlockRendering() { if (mRoot != null) { mRoot.unlockRenderThread(); } }}
自定义Texture
ColorTexture
// ColorTexture 就是一个填充特定颜色的色块,代码很简单,包括颜色和尺寸,已经绘制的方法;public class ColorTexture implements Texture { private final int mColor; private int mWidth; private int mHeight; public ColorTexture(int color) { mColor = color; mWidth = 1; mHeight = 1; } @Override public void draw(GLCanvas canvas, int x, int y) { draw(canvas, x, y, mWidth, mHeight); } @Override public void draw(GLCanvas canvas, int x, int y, int w, int h) { canvas.fillRect(x, y, w, h, mColor); } @Override public boolean isOpaque() { return Utils.isOpaque(mColor); } public void setSize(int width, int height) { mWidth = width; mHeight = height; } @Override public int getWidth() { return mWidth; } @Override public int getHeight() { return mHeight; }}
StringTexture
// StringTexture 提供了文本内容的绘制方法,可以设置文本内容、字体大小,颜色,也很简单;public class StringTexture extends CanvasTexture { private final String mText; private final TextPaint mPaint; private final FontMetricsInt mMetrics; private StringTexture(String text, TextPaint paint, FontMetricsInt metrics, int width, int height) { super(width, height); mText = text; mPaint = paint; mMetrics = metrics; } public static TextPaint getDefaultPaint(float textSize, int color) { TextPaint paint = new TextPaint(); paint.setTextSize(textSize); paint.setAntiAlias(true); paint.setColor(color); paint.setShadowLayer(2f, 0f, 0f, Color.BLACK); return paint; } public static StringTexture newInstance( String text, float textSize, int color) { return newInstance(text, getDefaultPaint(textSize, color)); } public static StringTexture newInstance( String text, float textSize, int color, float lengthLimit, boolean isBold) { TextPaint paint = getDefaultPaint(textSize, color); if (isBold) { paint.setTypeface(Typeface.defaultFromStyle(Typeface.BOLD)); } if (lengthLimit > 0) { text = TextUtils.ellipsize( text, paint, lengthLimit, TextUtils.TruncateAt.END).toString(); } return newInstance(text, paint); } private static StringTexture newInstance(String text, TextPaint paint) { FontMetricsInt metrics = paint.getFontMetricsInt(); int width = (int) Math.ceil(paint.measureText(text)); int height = metrics.bottom - metrics.top; // The texture size needs to be at least 1x1. if (width <= 0) width = 1; if (height <= 0) height = 1; return new StringTexture(text, paint, metrics, width, height); } @Override protected void onDraw(Canvas canvas, Bitmap backing) { canvas.translate(0, -mMetrics.ascent); canvas.drawText(mText, 0, 0, mPaint); }}
BitmapTexture
// BitmapTexture 提供了图片的绘制方法,细节都在UploadedTexture里,这是处理起来最麻烦的Texture了public class BitmapTexture extends UploadedTexture { protected Bitmap mContentBitmap; public BitmapTexture(Bitmap bitmap) { this(bitmap, false); } public BitmapTexture(Bitmap bitmap, boolean hasBorder) { super(hasBorder); Assert.assertTrue(bitmap != null && !bitmap.isRecycled()); mContentBitmap = bitmap; } @Override protected void onFreeBitmap(Bitmap bitmap) { if (!inFinalizer()) { bitmap.recycle(); } } @Override protected Bitmap onGetBitmap() { return mContentBitmap; } public Bitmap getBitmap() { return mContentBitmap; }}
说明
下一篇会细化介绍
0 0
- Android OpenGL ES 2.0学习研究 (一)
- Android OpenGL ES 2.0-学习笔记(一)
- android opengl es学习总结一:基础知识
- android opengl es学习总结一:基础知识
- openGL ES学习一
- Android 学习OpenGL ES
- OpenGL ES 2.0 学习笔记(一)
- Android opengl es 2.0怎么学习
- Android opengl es 2.0怎么学习
- Android OpenGL ES 2.0入门学习 1
- opengl es学习笔记一
- IOS openGL es 学习一
- Android OpenGL ES 应用(一)
- android opengl es从零开始(一)
- android OpenGL ES 2.0
- OpenGL ES for Android研究总结
- OpenGL ES for Android研究总结
- 利用OpenGL ES进行Android手游录屏研究
- leetcode 476. Number Complement
- spring boot入门
- 串口
- 【Leetcode】111. Minimum Depth of Binary Tree
- css 中的浮动
- Android OpenGL ES 2.0学习研究 (一)
- LDD3源码分析之访问控制
- HTML name、id、class 的区别
- caffe增加自己的layer实战(上)--caffe学习(10)
- Webview File域同源策略绕过漏洞
- Serializable学习
- unity3d中脚本生命周期(MonoBehaviour lifecycle)
- The difference between the request time and the current time is too large.
- SpringMVC中的参数自动匹配