• 利用EGL在android上使用C/C++写OpenGL ES程序


    很多教程都是在C/C++写的OpenGL的代码,其中有很多优秀的框架,除了前面提过的Assimp库外,还有很强大的库glm,从另外一个角度来看,在学习EGL的时候,很多的资料都是C语言的代码,我在android上写OpenGL ES的代码似乎从来没见过那些代码,不使用一下总觉得缺少点什么。

    事实上,Android在native层构建OpenGL环境的步骤就如同前面博客OpenGL ES EGL介绍中讲过的这样,在Java层,与EGL相关的操作我们也看不到,其实都是由GLSurfaceView封装好了。

    因此这篇博客就研究一下怎么使用Native代码写OpenGL ES代码和如何利用EGL自己创建OpenGL的环境。最终包含:

    1.使用Native代码+GLSurfaceView写的六边形,这里会介绍到glm库。

    2.Java代码自己写一个功能类似GLSurfaceView的类,主要是在java层使用EGL自己创建OpenGL的环境,思想上参考了GLSurfaceView的源码。

    3.全部使用native代码写的六边形。Java层仅用了SurfaceView,余下的功能全部在native层实现。

    先贴上效果图,这三幅图分别是上面描述的三个程序执行得到的结果。
    第一幅图是第一篇写OpenGL入门的时候使用的Demo,我用本博客第二部分写的MySurfaceView替换了系统自带的GLSurfaceView执行的效果。
    第一幅图和三幅图也是画一个六边形,只是没有旋转,大小并且都是用AndroidStudio创建的项目。

    pic


    使用Native代码+GLSurfaceView

    这种方式其实就是将GLSurfaceView.Renderer的几个回调函数在native层实现,算是体验C/C++写OpenGL代码的第一步吧。Render类大概就是下面的样子

        public void onSurfaceCreated(GL10 gl, EGLConfig config) {
            String vetexShaderStr = LoadShaderStr(mContext, R.raw.vshader);
            String fragmentShaderStr = LoadShaderStr(mContext, R.raw.fshader);
          // native
            nativeInit(vetexShaderStr, fragmentShaderStr);
        }
        @Override
        public void onDrawFrame(GL10 gl) {
          // native
            nativeDraw(mAngleX, mAngleY);
        }
        @Override
        public void onSurfaceChanged(GL10 gl, int width, int height) {
          // native
            nativeSurfaceChanged(width, height);
        }
        public static native void nativeInit(String vertexShaderCode, String fragmentShaderCode);
        private static native void nativeDraw(float angleX, float angleY);
        private static native void nativeSurfaceChanged(int width, int height);
    }

    在native层要做的事情其实和前面用java语言写的代码是类似的,无外乎就是写一个六边形的类,函数名也是一致的,因此写起来很简单。

    使用Java代码的时候在android.opengl包中有个很方便的类Matrix,里面封装了矩阵的相关操作,设置投影、摄像机矩阵、矩阵相乘等操作都被封装好了,在C/C++中可没有这个类,不过也不是难事。Android Native Development Kit Cookbook一书的作者就封装了相关操作。

    #ifndef MATRIX_H
    #define MATRIX_H
    
    #include <math.h>
    
    #define MYPI 3.14159265358979323846
    
    void translate_matrix(float tx, float ty, float tz, float *M);
    void scale_matrix(float xs, float ys, float zs, float *M);
    void rotate_matrix(float angle, float x, float y, float z, float *M);
    void perspective_matrix(float fovy, float aspect, float znear, float zfar, float *M);
    void multiply_matrix(float *A, float *B, float *M);
    
    #endif
    
    
    
    #include "matrix.h"
    
    //all matrix are in column-major order
    //the 4x4 matrix are stored in an array as shown below
    //0-15 indicates the array index
    //0 4   8   12
    //1 5   9   13
    //2 6   10  14
    //3 7   11  15
    
    
    void load_identity(float *M) {
        for (int i = 0; i < 16; ++i) {
            M[i] = 0.0f;
        }
        M[0] = 1.0f;
        M[5] = 1.0f;
        M[10] = 1.0f;
        M[15] = 1.0f;
    }
    
    //return translation matrix
    //  1   0   0   tx
    //  0   1   0   ty
    //  0   0   1   tz
    //  0   0   0   1
    void translate_matrix(float tx, float ty, float tz, float *M) {
        load_identity(M);
        M[12] = tx;
        M[13] = ty;
        M[14] = tz;
    }
    
    //return scaling matrix
    //  sx  0   0   0
    //  0   sy  0   0
    //  0   0   sz  0
    //  0   0   0   1
    void scale_matrix(float sx, float sy, float sz, float *M) {
        load_identity(M);
        M[0] *= sx;
        M[5] *= sy;
        M[10] *= sz;
    }
    
    //return rotation matrix
    //R = Rx*Ry*Rz
    //          1   0           0       0
    //  Rx =    0   cosx        -sinx   0
    //          0   sinx        cosx    0
    //          0   0           0       1
    
    //      cosy        0       siny        0
    //      0           1       0           0
    //  Ry =-siny       0       cosy        0
    //      0           0       0           1
    //
    //      cosz        -sinz   0       0
    //  Rz= sinz        cosz    0       0
    //      0           0       1       0
    //      0           0       0       1
    //refer to http://lignumcad.sourceforge.net/doc/en/HTML/SOHTML/TechnicalReference.html
    //for detailed info
    
    void rotate_matrix(float angle, float x, float y, float z, float *M) {
        double radians, c, s, c1, u[3], length;
        int i, j;
    
        radians = (angle * MYPI) / 180.0;
        c = cos(radians);
        s = sin(radians);
        c1 = 1.0 - cos(radians);
        length = sqrt(x * x + y * y + z * z);
        u[0] = x / length;
        u[1] = y / length;
        u[2] = z / length;
    
        for (i = 0; i < 16; i++) {
            M[i] = 0.0;
        }
        M[15] = 1.0;
    
        for (i = 0; i < 3; i++) {
            M[i * 4 + (i + 1) % 3] = u[(i + 2) % 3] * s;
            M[i * 4 + (i + 2) % 3] = -u[(i + 1) % 3] * s;
        }
        for (i = 0; i < 3; i++) {
            for (j = 0; j < 3; j++) {
                M[i * 4 + j] += c1 * u[i] * u[j] + (i == j ? c : 0.0);
            }
        }
    }
    
    
    /* simulate gluPerspectiveMatrix
     * //return perspective projection matrix
     *  cot(fovy/2) / aspect        0               0                   0
     *  0                       cot(fovy/2)         0                   0
     *  0                           0       (znear+zfar)/(znear-zfar)   2*znear*zfa/(znear-zfar)
     *  0                           0               -1                  0
     */
    void perspective_matrix(float fovy, float aspect, float znear, float zfar, float *M) {
        int i;
        double f;
    
        load_identity(M);
    
        f = 1.0/tan(fovy * 0.5);
    
        M[0] = f / aspect;
        M[5] = f;
        M[10] = (znear + zfar) / (znear - zfar);
        M[11] = -1.0;
        M[14] = (2.0 * znear * zfar) / (znear - zfar);
        M[15] = 0.0;
    }
    
    //Multiplies A by B and output to C, a tmp buffer is used to support in-place multiplication
    void multiply_matrix(float *A, float *B, float *C) {
        int i, j, k;
        float tmpM[16];
    
        for (i = 0; i < 4; i++) {
            for (j = 0; j < 4; j++) {
                tmpM[j * 4 + i] = 0.0;
                for (k = 0; k < 4; k++) {
                    tmpM[j * 4 + i] += A[k * 4 + i] * B[j * 4 + k];
                }
            }
        }
        for (i = 0; i < 16; i++) {
            C[i] = tmpM[i];
        }
    }
    

    不过我觉得这都是不必要的工作,很浪费时间,事实上有一个非常强大的数学库glm(OpenGL Mathematics

    )主要还体现在它是为OpenGL开发人员量身定做的,和使用着色器语言一样,使用很方便。我在这个Demo中主要用了它的设置摄像机矩阵、投影矩阵。

    #include "glm/mat4x4.hpp"
    #include "glm/ext.hpp"
    glm::mat4 projection;
    glm::mat4 view;
    //glm::mat4 module;
    projection = glm::ortho(-1.0f, 1.0f, -(float) height / width, (float) height / width, 5.0f,
                                7.0f);
    view = glm::lookAt(glm::vec3(0.0f, 0.0f, 6.0f),
                           glm::vec3(0.0f, 0.0f, 0.0f),
                           glm::vec3(0.0f, 1.0f, 0.0f));
    // 矩阵相乘太方便
    glm::mat4 mvpMatrix = projection * view/* * module*/;
    // 装换成数组形式
    float *mvp = (float *) glm::value_ptr(mvpMatrix);
    mShape.draw(mvp);

    代码在这里:https://github.com/qhyuan1992/OpenGL-ES/tree/master/NativeGLESView

    在Java层使用EGL

    其实在利用OpenGL ES进行Android手游录屏研究这篇博客中已经接触到了Java层的EGL使用了,GLSurfaceView继承自SurfaceView,我们用它来写OpenGL代码很方便,就是因为它已经封装好了创建EGL环境的部分了,如果看GLSurfaceView的代码必然可以看到前面讲EGL部分的时候创建EGL环境的相关函数调用。

    不过由于GLSurfaceView代码比较多,下面是我自己写的一个类似GLSurfaceView的类,写的很简略,用其来替代系统的GLSurfaceView即可。显然这样做用来了解Java层EGL的使用是足够的。

    里面有部分代码我是参考GLSurfaceView的源码的。

    package com.example.opengles_circle;
    import java.lang.ref.WeakReference;
    
    import javax.microedition.khronos.egl.EGL10;
    import javax.microedition.khronos.egl.EGLConfig;
    import javax.microedition.khronos.egl.EGLContext;
    import javax.microedition.khronos.egl.EGLDisplay;
    import javax.microedition.khronos.egl.EGLSurface;
    
    import android.content.Context;
    import android.view.SurfaceHolder;
    import android.view.SurfaceView;
    
    public class MyGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
        public SurfaceHolder mHolder;
        public Renderer mRenderer;
        private final WeakReference<MyGLSurfaceView> mThisWeakRef = new WeakReference<MyGLSurfaceView>(this);
        Object mLock = new Object();
        private GLThread mThread;
        public MyGLSurfaceView(Context context) {
            super(context);
            mHolder = getHolder();
            mHolder.addCallback(this);
        }
    
        public void setRenderer(Renderer renderer) {
            mRenderer = renderer;
            mThread = new GLThread(mThisWeakRef);
            mThread.start();
        }
    
        public void surfaceCreated(SurfaceHolder holder) {
            mThread.surfaceCreated();
        }
    
        public void surfaceDestroyed(SurfaceHolder holder) {
            mThread.surfaceDestroyed();
        }
    
        public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
            mThread.surfaceChanged(w, h);
        }
    
        public void stopRender() {
            // 设置停止标示,notify
        }
    
        public interface Renderer {
            void onSurfaceCreated();
    
            void onSurfaceChanged(int width, int height);
    
            void onDrawFrame();
        }
    
    }
    
    class GLThread extends Thread {
        private Object lock = new Object();
        private EGLHelper mEglHelper;
        private boolean mHasSurface = false;
        public int mWidth;
        public int mHeight;
        private WeakReference<MyGLSurfaceView> mGLSurfaceViewWeakRef;
    
         GLThread(WeakReference<MyGLSurfaceView> glSurfaceViewWeakRef) {
             super();
             mWidth = 0;
             mHeight = 0;
             mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
         }
    
        @Override
        public void run() {
            try {
                doThread();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        private void doThread() throws InterruptedException {
            mEglHelper = new EGLHelper(mGLSurfaceViewWeakRef);
            MyGLSurfaceView view = mGLSurfaceViewWeakRef.get();
            // 确保有了Surface
            synchronized (lock) {
                if (!mHasSurface) {
                    lock.wait();
                }
            }
            mEglHelper.start();
            /*----实际上是在一个循环里面针对不同的事件(onSurfaceCreated/onSurfaceChanged/onDrawFrame/terminal)执行不同的回调----*/
            /*-----所有情况都执行完后,wait,当某个事件发生后标志置位,然后调用notify----*/
            view.mRenderer.onSurfaceCreated();
            // 也要确保在surfaceChanged之后执行
            view.mRenderer.onSurfaceChanged(mWidth, mHeight);
            view.mRenderer.onDrawFrame();
            mEglHelper.swap();
        }
    
        public void surfaceCreated() {
        }
    
        public void surfaceChanged(int w, int h) {
            mWidth = w;
            mHeight = h;
            synchronized (lock) {
                mHasSurface = true;
                lock.notifyAll();
            }
        }
    
        public void surfaceDestroyed() {
            synchronized (lock) {
                mHasSurface = false;
                lock.notifyAll();           
            }
        }
    }
    
    class EGLHelper{
        public WeakReference<MyGLSurfaceView> mGLSurfaceViewWeakRef;
        private EGL10 mEgl;
        private EGLDisplay mEglDisplay;
        private EGLConfig mEglConfig;
        private EGLContext mEglContext;
        private EGLSurface mEglSurface;
    
        public EGLHelper(WeakReference<MyGLSurfaceView> glSurfaceViewWeakRef) {
            mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
        }
    
      // start函数中就是创建EGL环境的步骤
        public void start(){
            int[] num_config = new int[1];
            EGLConfig[] configs = new EGLConfig[1];
            int[] configSpec = { EGL10.EGL_RED_SIZE, 8, 
                    EGL10.EGL_GREEN_SIZE, 8,
                    EGL10.EGL_BLUE_SIZE, 8,
                    EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, EGL10.EGL_NONE };
    
            this.mEgl = (EGL10) EGLContext.getEGL();
    
            mEglDisplay = this.mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
            this.mEgl.eglInitialize(mEglDisplay, null);
    
            this.mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, num_config);
    
            mEglConfig = configs[0];
            mEglSurface = this.mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig,
                    mGLSurfaceViewWeakRef.get().mHolder, null);
    
            int[] attrs = {0x3098, 2, EGL10.EGL_NONE}; // EGL_CONTEXT_CLIENT_VERSION
            mEglContext = this.mEgl.eglCreateContext(mEglDisplay, mEglConfig,
                    EGL10.EGL_NO_CONTEXT, attrs);
    
            this.mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
        }
    
        public void swap() {
            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
        }
    
        public void destroySurface() {
            if (mEglSurface != null) {
                mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
                mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
                mEglSurface = null;
            }
            if (mEglContext != null) {
                mEgl.eglDestroyContext(mEglDisplay, mEglContext);
                mEglContext = null;
            }
            if (mEglDisplay != null) {
                mEgl.eglTerminate(mEglDisplay);
                mEglDisplay = null;
            }
        }
    }

    可以看到当调用setRender时就开启了一个子线程GLThread,不过这个线程没有立即执行而是wait直到surface创建成功。

    EGLHelper就是一个工具类,里面的start函数就包含了创建EGL环境的步骤,等待EGL上下文创建好了,在执行 view.mRenderer.onDrawFrame();回调中其实就是我们要实现的部分,在里面进行OpenGL绘图,然后在执行EGLHelper的swap函数,将后台缓冲中的内容显示到native_window也就是屏幕上去。

    一般来说在这个GLThread线程中是一个无限循环,根据收到的不同状态值,执行对应的操作,每执行一轮就会调用wait直到其他的状态被设置后notify唤醒,在循环执行。

    在这里注意一下eglCreateWindowSurface函数中的native_window参数。它是一个Object对象,它的实现在com.google.android.gles_jni.EGLImpl.java(EGLImpl.java),先记住native_window可以传入的参数是SurfaceView、SurfaceHolder或者Surface,最终都转换成了Surface,Surface在Java层代表这一个窗口。

    EGLSurface  eglCreateWindowSurface(EGLDisplay display, EGLConfig config, Object native_window, int[] attrib_list) {
      Surface sur = null;
            if (native_window instanceof SurfaceView) {
                SurfaceView surfaceView = (SurfaceView)native_window;
                sur = surfaceView.getHolder().getSurface();
            } else if (native_window instanceof SurfaceHolder) {
                SurfaceHolder holder = (SurfaceHolder)native_window;
                sur = holder.getSurface();
            } else if (native_window instanceof Surface) {
    
              // .......
    }

    在native层使用EGL

    在native层使用EGL创建EGL环境并且使用C/C++写OpenGL代码,思路就是结合前面两个Demo,在native层创建EGL环境和在Java层是类似的思路,只是有些API可能不一样,并且还用到了很多NDK的知识,

    代码在这里https://github.com/qhyuan1992/OpenGL-ES/tree/master/NativeGLESViewWithEGL

    核心部分的代码

    
    void Renderer::requestInitEGL(ANativeWindow * pWindow) {
        LOGI(1, "-------requestInitEGL");
        pthread_mutex_lock(&mMutex);
        mWindow = pWindow;
      // mEnumRenderEvent表示事件
        mEnumRenderEvent = RE_SURFACE_CHANGED;
        LOGI(1, "-------mEnumRenderEvent=%d", mEnumRenderEvent);
        pthread_mutex_unlock(&mMutex);
      // 唤醒处于wait状态的线程,继续函数onRenderThreadRun的执行,onRenderThreadRun是线程的入口函数
        pthread_cond_signal(&mCondVar);
    }
    
    // 每次需要绘制的时候调用,实际上会调用到
    void Renderer::requestRenderFrame() {
        pthread_mutex_lock(&mMutex);
        mEnumRenderEvent = RE_DRAW_FRAME;
        pthread_mutex_unlock(&mMutex);
        pthread_cond_signal(&mCondVar);
    }
    
    void Renderer::requestDestroy() {
        pthread_mutex_lock(&mMutex);
        mEnumRenderEvent = RE_EXIT;
        pthread_mutex_unlock(&mMutex);
        pthread_cond_signal(&mCondVar);
    }
    
    void Renderer::onRenderThreadRun() {
        mISRenderering = true;
        while(mISRenderering) {
            pthread_mutex_lock(&mMutex);
            // 每完成一个事件就wait在这里直到有其他事件唤醒
            pthread_cond_wait(&mCondVar, &mMutex);
    
            LOGI(1, "-------this mEnumRenderEvent is %d", mEnumRenderEvent);
            switch (mEnumRenderEvent) {
                case RE_SURFACE_CHANGED:
                    LOGI(1, "-------case RE_SURFACE_CHANGED");
                    mEnumRenderEvent = RE_NONE;
                    pthread_mutex_unlock(&mMutex);
                    initEGL();
                    nativeSurfaceCreated();
                    nativeSurfaceChanged(mWidth, mHeight);
                    break;
                case RE_DRAW_FRAME:
                    mEnumRenderEvent = RE_NONE;
                    pthread_mutex_unlock(&mMutex);
                    // draw
                    // 这个函数留给真实的GLES绘制操作。
                    nativeDraw();
                    eglSwapBuffers(mDisplay, mSurface);
                    break;
                case RE_EXIT:
                    mEnumRenderEvent = RE_NONE;
                    pthread_mutex_unlock(&mMutex);
                    terminateDisplay();
                    mISRenderering = false;
                    break;
                default:
                    mEnumRenderEvent = RE_NONE;
                    pthread_mutex_unlock(&mMutex);
            }
        }
    }

    下面我也把native层创建EGL环境的部分列出来,显然和Java层是类似的。

    void Renderer::initEGL() {
        const EGLint attribs[] = {
                EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
                EGL_BLUE_SIZE, 8,
                EGL_GREEN_SIZE, 8,
                EGL_RED_SIZE, 8,
                EGL_NONE
        };
        EGLint width, height, format;
        EGLint numConfigs;
        EGLConfig config;
        EGLSurface surface;
        EGLContext context;
    
        EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    
        eglInitialize(display, 0, 0);
    
        eglChooseConfig(display, attribs, &config, 1, &numConfigs);
    
        surface = eglCreateWindowSurface(display, config, mWindow, NULL);
        EGLint attrs[]= {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
        context = eglCreateContext(display, config, NULL, attrs);
    
        if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
            LOGI(1, "------EGL-FALSE");
            return ;
        }
    
        eglQuerySurface(display, surface, EGL_WIDTH, &width);
        eglQuerySurface(display, surface, EGL_HEIGHT, &height);
    
        mDisplay = display;
        mSurface = surface;
        mContext = context;
        mWidth = width;
        mHeight = height;
        LOGI(1, "%d, height:%d", mWidth, mHeight);
    
    }

    在Java层继承自一个SurfaceView

    public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
        public MySurfaceView(Context context) {
            super(context);
            getHolder().addCallback(this);
        }
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            nativeRequestRender();
            return super.onTouchEvent(event);
        }
        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {
            setRender();
        }
        @Override
        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
            nativeSurfaceChanged(surfaceHolder.getSurface());
        }
        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
            nativeSurfaceDestroyed();
        }
        private void setRender() {
            nativeStartRender();
        }
        private static native void nativeSurfaceChanged(Surface surface);
        private static native void nativeSurfaceDestroyed();
        private static native void nativeStartRender();
        private static native void nativeRequestRender();
        static {
            System.loadLibrary("NativeWithEGL");
        }
    }

    转而在native层执行SurfaceHolder.Callback的回调,注意和前面的GLSurfaceView.Renderer的回调。

    接下来在合适的时刻调用Renderer的request***函数,发出某种消息,比如执行requestRenderFrame函数,也就是发出了要绘制一帧的消息,那么在绘制线程中,就去执行OpenGL ES绘制操作代码。

    ANativeWindow * mWindow;
    Renderer * mRenderer;
    extern "C" {
    JNIEXPORT void JNICALL
    Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeStartRender(JNIEnv *env, jclass type) {
        mRenderer = new Renderer();
        mRenderer->start();
    }
    JNIEXPORT void JNICALL
    Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeSurfaceChanged(JNIEnv *env, jclass type, jobject surface) {
        mWindow = ANativeWindow_fromSurface(env, surface);
        // surfacechange时 发送SurfaceChanged消息,此时创建egl环境的消息
        mRenderer->requestInitEGL(mWindow);
    }
    
    JNIEXPORT void JNICALL
    Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeSurfaceDestroyed(JNIEnv *env, jclass type) {
        mRenderer->requestDestroy();
        ANativeWindow_release(mWindow);
        delete mRenderer;
    }
    JNIEXPORT void JNICALL
    Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeRequestRender(JNIEnv *env, jclass type){
        mRenderer->requestRenderFrame();
    }
    }

    在Native层ANativeWindow对象代表一个窗口,看起来好像和Java层的Surface是对应着的,native层的ANativeWindow的指针是Java层的Surface对象的一个成员变量,这也是持久保存native层数据的常用方式,在NDK中有很多类似于ANativeWindow_fromSurface的函数,将Java层的独享转换为native层的对象,都是类似的方式。

    Java代码自己写一个功能类似GLSurfaceView的类

  • 相关阅读:
    luogu P1019 单词接龙
    luogu P4137 Rmq Problem / mex
    Virtualbox 修改硬盘的序列号等信息 例
    httpHandlers path="*.sky"
    Oracle告Google输了
    %STSADM% -o addsolution -filename AEMediaPlayerWebpart.wsp
    placeholder
    String强制转换为Date,freemarker标签里date数据的显示问题
    eclipse配置JDK和设置编译版本的几种方法
    httpd 系统错误 无法启动此程序,因为计算机中丢失VCRUNTIME140.dll
  • 原文地址:https://www.cnblogs.com/qhyuan1992/p/6298180.html
Copyright © 2020-2023  润新知