深入浅出 - Android系统移植与平台开发(十) - led HAL简单设计案例分析
作者:唐老师,华清远见嵌入式学院讲师。
通过前两节HAL框架分析和JNI概述,我们对Android提供的Stub HAL有了比较详细的了解了,下面我们来看下led的实例,写驱动点亮led灯,就如同写程序,学语言打印HelloWorld一样,如果说打印HelloWorld是一门新语言使用的第一声吆喝,那么点亮led灯就是我们学习HAL的一座灯塔,指挥我们在后面的复杂的HAL代码里准确找到方向。
LedHAL实例架构
上图描述了我们Led实例的框架层次:
l LedDemo.java:是我们写的Android应用程序
l LedService.java:是根据Led HAL封装的Java框架层的API,主要用于向应用层提供框架层API,它属于Android的框架层
l libled_runtime.so:由于Java代码不能访问HAL层,该库是LedService.java对应的本地代码部分
l led.default.so:针对led硬件的HAL代码
LedDemo通过LedService提供的框架层API访问Led设备,LedService对于LedDemo应用程序而言是Led设备的服务提供者,LedService运行在Dalvik中没有办法直接访问Led硬件设备,它只能将具体的Led操作交给本地代码来实现,通过JNI来调用Led硬件操作的封装库libled_runtime.so,由HAL Stub框架可知,在libled_runtime.so中首先查找注册为led的硬件设备module,找到之后保存其操作接口指针在本地库中等待框架层LedService调用。led.default.so是HAL层代码,它是上层操作的具体实施者,它并不是一个动态库(也就是说它并没有被任何进程加载并链接),它只是在本地代码查找硬件设备module时通过ldopen”杀鸡取卵”找module,返回该硬件module对应的device操作结构体中封装的函数指针。
其调用时序如下:
Led HAL实例代码分析
我们来看下led实例的目录结构:
主要文件如下:
com.hello.LedService.cpp:它在frameworks/services/jni目录下,是的Led本地服务代码
led.c:HAL代码
led.h:HAL代码头文件
LedDemo.java:应用程序代码
LedService.java:Led框架层服务代码
在Android的源码目录下,框架层服务代码应该放在frameworks/services/java/包名/目录下,由Android的编译系统统一编译生成system/framework/services.jar文件,由于我们的测试代码属于厂商定制代码,尽量不要放到frameworks的源码树里,我将其和LedDemo应用程序放在一起了,虽然这种方式从Android框架层次上不标准。
另外,本地服务代码的文件名要和对应的框架层Java代码的名字匹配(包名+类文件名,包目录用“_“代替)。有源码目录里都有对应的一个Android.mk文件,它是Android编译系统的指导文件,用来编译目标module。
1) Android.mk文件分析
先来看下led源码中①号Android.mk:
[plain] view plaincopyprint?
1. include $(call all-subdir-makefiles)
代码很简单,表示包含当前目录下所有的Android.mk文件
先来看下led_app目录下的③号Android.mk:
[plain] view plaincopyprint?
1. # 调用宏my-dir,这个宏返回当前Android.mk文件所在的路径
2. LOCAL_PATH:= $(call my-dir)
3.
4. # 包含CLEAR_VARS变量指向的mk文件build/core/clear_vars.mk,它主要用来清除编译时依赖的编译变量
5. include $(CLEAR_VARS)
6.
7. # 指定当前目标的TAG标签,关于其作用见前面Android编译系统章节
8. LOCAL_MODULE_TAGS := user
9.
10. # 当前mk文件的编译目标模块
11. LOCAL_PACKAGE_NAME := LedDemo
12.
13. # 编译目标时依赖的源码,它调用了一个宏all-java-files-under,该宏在build/core/definitions.mk中定义
14. # 表示在当前目录下查找所有的java文件,将查找到的java文件返回
15. LOCAL_SRC_FILES := $(callall-java-files-under, src)
16.
17. # 在编译Android应用程序时都要指定API level,也就是当前程序的编译平台版本
18. # 这里表示使用当前源码的版本
19. LOCAL_SDK_VERSION := current
20.
21. # 最重要的就是这句代码,它包含了一个文件build/core/package.mk,根据前面设置的编译变量,编译生成Android包文件,即:apk文件
22. include $(BUILD_PACKAGE)
上述代码中都加了注释,基本上每一个编译目标都有类似上述的编译变量的声明:
LOCAL_MODULE_TAGS
LOCAL_PACKAGE_NAME
LOCAL_SRC_FILES
由于所有的Android.mk最终被编译系统包含,所以在编译每个目标模块时,都要通过LOCAL_PATH:= $(call my-dir)指定当前目标的目录,然后调用include $(CLEAR_VARS)先清除编译系统依赖的重要的编译变量,再生成新的编译变量。
让我们来看看LedDemo目标对应的源码吧。
2) LedDemo代码分析
学习过Android应用的同学对其目录结构很熟悉,LedDemo的源码在src目录下。
@ led_app/src/com/farsight/LedDemo.java:
[java] view plaincopyprint?
1. package com.hello;
2.
3. import com.hello.LedService;
4.
5. import com.hello.R;
6.
7. importandroid.app.Activity;
8.
9. importandroid.os.Bundle;
10.
11. importandroid.util.Log;
12.
13. importandroid.view.View;
14.
15. import android.view.View.OnClickListener;
16.
17. importandroid.widget.Button;
18.
19.
20.
21. public classLedDemo extends Activity {
22. privateLedService led_svc;
23. private Buttonbtn;
24. private booleaniflag = false;
25. private Stringtitle;
26.
27. /** Calledwhen the activity is first created. */
28. @Override
29. public void onCreate(Bundle savedInstanceState) {
30. super.onCreate(savedInstanceState);
31. setContentView(R.layout.main);
32.
33. Log.i("Java App", "OnCreate");
34. led_svc =new LedService();
35. btn =(Button) this.findViewById(R.id.Button01);
36. this.btn.setOnClickListener(new OnClickListener() {
37. public void onClick(View v) {
38. Log.i("Java App", "btnOnClicked");
39. if (iflag) {
40. title = led_svc.set_off();
41. btn.setText("Turn On");
42. setTitle(title);
43. iflag = false;
44. } else {
45. title = led_svc.set_on();
46. btn.setText("Turn Off");
47. setTitle(title);
48. iflag = true;
49. }
50. }
51. });
52. }
53. }
代码很简单,Activity上有一个按钮,当Activity初始化时创建LedService对象,按钮按下时通过LedService对象调用其方法set_on()和set_off()。
3) LedService代码分析
我们来看下LedService的代码:
@led_app/src/com/farsight/LedService.java:
[java] view plaincopyprint?
1. package com.hello;
2. import android.util.Log;
3.
4. public class LedService {
5.
6. /*
7. * loadnative service.
8. */
9. static { // 静态初始化语言块,仅在类被加载时被执行一次,通常用来加载库
10. Log.i ("Java Service" , "Load Native Serivce LIB" );
11. System.loadLibrary ( "led_runtime" );
12. }
13.
14. // 构造方法
15. public LedService() {
16. int icount ;
17.
18. Log.i ("Java Service" , "do init Native Call" );
19. _init ();
20. icount =_get_count ();
21. Log.d ("Java Service" , "led count = " + icount );
22. Log.d ("Java Service" , "Init OK " );
23. }
24.
25. /*
26. * LED nativemethods.
27. */
28. public Stringset_on() {
29. Log.i ("com.hello.LedService" , "LED On" );
30. _set_on();
31. return"led on" ;
32. }
33.
34. public String set_off() {
35. Log.i ("com.hello.LedService" , "LED Off" );
36. _set_off();
37. return"led off" ;
38. }
39.
40. /*
41. * declare all the native interface.
42. */
43. private static native boolean _init();
44. private static native int _set_on();
45. private static native int _set_off();
46. private static native int _get_count();
47.
48. }
通过分析上面代码可知LedService的工作:
l 加载本地服务的库代码
l 在构造方法里调用_init本地代码,对Led进行初始化,并调用get_count得到Led灯的个数
l 为LedDemo应用程序提供两个API:set_on和set_off,这两个API方法实际上也是交给了本地服务代码来操作的
由于Java代码无法直接操作底层硬件,通过JNI方法将具体的操作交给本地底层代码实现,自己只是一个API Provider,即:服务提供者。
让我们来到底层本地代码,先看下底层代码的Android.mk文件:
@ frameworks/Android.mk:
[plain] view plaincopyprint?
1. LOCAL_PATH:= $(call my-dir)
2. include $(CLEAR_VARS)
3.
4. LOCAL_MODULE_TAGS := eng
5. LOCAL_MODULE:= libled_runtime # 编译目标模块
6. LOCAL_SRC_FILES:= \
7. services/jni/com_farsight_LedService.cpp
8.
9.
10. LOCAL_SHARED_LIBRARIES := \ # 编译时依赖的动态库
11. libandroid_runtime \
12. libnativehelper \
13. libcutils \
14. libutils \
15. libhardware
16.
17. LOCAL_C_INCLUDES += \ #编译时用到的头文件目录
18. $(JNI_H_INCLUDE)
19.
20. LOCAL_PRELINK_MODULE := false # 本目标为非预链接模块
21. include $(BUILD_SHARED_LIBRARY) # 编译生成共享动态库
结合前面分析的Android.mk不难看懂这个mk文件。之前的mk文件是编译成Android apk文件,这儿编译成so共享库,所以LOCAL_MODULE和include $(BUILD_SHARED_LIBRARY)与前面mk文件不同,关于Android.mk文件里的变量作用,请查看Android编译系统章节。
总而言之,本地代码编译生成的目标是libled_runtime.so文件。
4) Led本地服务代码分析
我们来看下本地服务的源码:
@ frameworks/services/jni/com_hello_LedService.cpp:
[cpp] view plaincopyprint?
1. #define LOG_TAG "LedService"
2. #include "utils/Log.h"
3. #include <stdlib.h>
4. #include <string.h>
5. #include <unistd.h>
6. #include <assert.h>
7. #include <jni.h>
8. #include "../../../hardware/led.h"
9.
10. static led_control_device_t *sLedDevice = 0;
11. static led_module_t* sLedModule=0;
12.
13. static jint get_count(void)
14. {
15. LOGI("%sE", __func__);
16. if(sLedDevice)
17. returnsLedDevice->get_led_count(sLedDevice);
18. else
19. LOGI("sLedDevice is null");
20. return 0;
21. }
22.
23.
24. static jint led_setOn(JNIEnv* env, jobject thiz) {
25. LOGI("%sE", __func__);
26. if(sLedDevice) {
27. sLedDevice->set_on(sLedDevice);
28. }else{
29. LOGI("sLedDevice is null");
30. }
31. return 0;
32. }
33.
34. static jint led_setOff(JNIEnv* env, jobject thiz) {
35. LOGI("%s E", __func__);
36. if(sLedDevice) {
37. sLedDevice->set_off(sLedDevice);
38. }else{
39. LOGI("sLedDevice is null");
40. }
41. return 0;
42. }
43.
44. static inline int led_control_open(const structhw_module_t* module,
45. structled_control_device_t** device) {
46. LOGI("%s E ", __func__);
47. returnmodule->methods->open(module,
48. LED_HARDWARE_MODULE_ID, (struct hw_device_t**)device);
49. }
50.
51. static jint led_init(JNIEnv *env, jclass clazz)
52. {
53. led_module_tconst * module;
54. LOGI("%s E ", __func__);
55. if(hw_get_module(LED_HARDWARE_MODULE_ID, (const hw_module_t**)&module) == 0){
56. LOGI("get Module OK");
57. sLedModule = (led_module_t *) module;
58. if(led_control_open(&module->common, &sLedDevice) != 0) {
59. LOGI("led_init error");
60. return-1;
61. }
62. }
63.
64. LOGI("led_init success");
65. return 0;
66 . }
67.
68.
69.
70. /*
71. *
72. * Array ofmethods.
73. * Each entryhas three fields: the name of the method, the method
74. * signature,and a pointer to the native implementation.
75. */
76. static const JNINativeMethod gMethods[] = {
77. {"_init", "()Z",(void*)led_init},
78. {"_set_on", "()I",(void*)led_setOn },
79. {"_set_off", "()I",(void*)led_setOff },
80. {"_get_count", "()I",(void*)get_count },
81. };
82.
83. static int registerMethods(JNIEnv* env) {
84. static constchar* const kClassName = "com/hello/LedService";
85. jclass clazz;
86. /* look upthe class */
87. clazz =env->FindClass(kClassName);
88. if (clazz ==NULL) {
89. LOGE("Can't find class %s\n", kClassName);
90. return-1;
91. }
92.
93. /* registerall the methods */
94. if(env->RegisterNatives(clazz, gMethods,
95. sizeof(gMethods) / sizeof(gMethods[0])) != JNI_OK)
96. {
97. LOGE("Failed registering methods for %s\n", kClassName);
98. return -1;
99. }
100. /* fill outthe rest of the ID cache */
101. return 0;
102. }
103.
104.
105.
106. /*
107. * This iscalled by the VM when the shared library is first loaded.
108. */
109. jint JNI_OnLoad(JavaVM* vm, void* reserved) {
110. JNIEnv* env= NULL;
111. jint result= -1;
112. LOGI("JNI_OnLoad");
113. if(vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
114. LOGE("ERROR: GetEnv failed\n");
115. gotofail;
116. }
117.
118. assert(env!= NULL);
119. if(registerMethods(env) != 0) {
120. LOGE("ERROR: PlatformLibrary nativeregistration failed\n");
121. gotofail;
122. }
123. /* success-- return valid version number */
124. result =JNI_VERSION_1_4;
125.
126. fail:
127. return result;
128. }
这儿的代码不太容易读,因为里面是JNI的类型和JNI特性的代码,看代码先找入口。LedService.java框架代码一加载就调用静态初始化语句块里的System.loadLibrary ( "led_runtime" ),加载libled_runtime.so,该库刚好是前面Android.mk文件的目标文件,也就是说LedService加载的库就是由上面的本地代码生成的。当一个动态库被Dalvik加载时,首先在Dalvik会回调该库代码里的JNI_OnLoad函数。也就是说JNI_OnLoad就是本地服务代码的入口函数。
JNI_OnLoad的代码一般来说是死的,使用的时候直接拷贝过来即可,vm->GetEnv会返回JNIEnv指针,而这个指针其实就是Java虚拟机的环境变量,我们可以通过该指针去调用JNI提供的方法,如FindClass等,调用registerMethods方法,在方法里通过JNIEnv的FindClass查找LedService类的引用,然后在该类中注册本地方法与Java方法的映射关系,上层Java代码可以通过这个映射关系调用到本地代码的实现。
RegisterNatives方法接收三个参数:
l 第一个参数jclass:要注册哪个类里的本地方法映射关系
l 第二个参数JNINativeMethod*:这是一个本地方法与Java方法映射数组,JNINativeMethod是个结构体,每个元素是一个Java方法到本地方法的映射。
[cpp] view plaincopyprint?
1. typedef struct {
2. constchar* name;
3. constchar* signature;
4. void*fnPtr;
5. } JNINativeMethod;
name:表示Java方法名
signature:表示方法的签名
fnPtr:Java方法对应的本地方法指针
l 第三个参数size:映射关系个数
由代码可知,Java方法与本地方法的映射关系如下:
Java方法 | 本地方法 |
void _init() | jint led_init(JNIEnv *env, jclass clazz) |
int _set_on() | jint led_setOn(JNIEnv* env, jobject thiz) |
int _set_off() | jint led_setOff(JNIEnv* env, jobject thiz) |
int _get_count() | jint get_count(void) |
通过上表可知,本地方法参数中默认会有两个参数:JNIEnv* env, jobject thiz,分别表示JNI环境和调用当前方法的对象引用,当然你也可以不设置这两个参数,在这种情况下你就不能访问Java环境中的成员。本地方法与Java方法的签名必须一致,返回值不一致不会造成错误。
现在我们再来回顾下我们的调用调用流程:
l LedDemo创建了LedService对象
l LedService类加载时加载了对应的本地服务库,在本地服务库里Dalvik自动调用JNI_OnLoad函数,注册Java方法和本地方法映射关系。
根据Java语言特点,当LedDemo对象创建时会调用其构造方法LedService()。
[cpp] view plaincopyprint?
1. // 构造方法
2. public LedService() {
3. int icount ;
4. Log.i ("Java Service" , "do init Native Call" );
5. _init ();
6. icount =_get_count ();
7. Log.d ("Java Service" , "led count = " + icount );
8. Log.d ("Java Service" , "Init OK " );
9. }
在LedService构造方法里直接调用了本地方法_init和_get_count(通过native保留字声明),也就是说调用了本地服务代码里的jint led_init(JNIEnv *env, jclass clazz)和jintget_count(void)。
在led_init方法里的内容就是我们前面分析HAL框架代码的使用规则了。
l 通过hw_get_module方法查到到注册为LED_HARDWARE_MODULE_ID,即:”led”的module模块。
l 通过与led_module关联的open函数指针打开led设备,返回其device_t结构体,保存在本地代码中,有的朋友可能会问,不是本地方法不能持续保存一个引用吗?由于device_t结构是在open设备时通过malloc分配的,只要当前进程不死,该指针一直可用,在这儿本地代码并没有保存Dalvik里的引用,保存的是mallco的分配空间地址,但是在关闭设备时记得要将该地址空间free了,否则就内存泄漏了。
l 拿到了led设备的device_t结构之后,当LedDemo上的按钮按下时调用LedService对象的set_on和set_off方法,这两个LedService方法直接调用了本地服务代码的对应映射方法,本地方法直接调用使用device_t指向的函数来间接调用驱动操作代码。
好吧,让我们再来看一个详细的时序图:
不用多解释了。
最后一个文件,HAL对应的Android.mk文件:
@ hardware/Android.mk:
[plain] view plaincopyprint?
1. LOCAL_PATH := $(call my-dir)
2. include $(CLEAR_VARS)
3.
4. LOCAL_C_INCLUDES += \
5. include/
6.
7. LOCAL_PRELINK_MODULE := false
8. LOCAL_MODULE_PATH := $(TARGET_OUT_SHARED_LIBRARIES)/hw
9. LOCAL_SHARED_LIBRARIES := liblog
10. LOCAL_SRC_FILES := led.c
11. LOCAL_MODULE := led.default
12. include $(BUILD_SHARED_LIBRARY)
注:LOCAL_PRELINK_MODULE:= false要加上,否则编译出错
指定目标名为:led.default
目标输入目录LOCAL_MODULE_PATH为:/system/lib/hw/,不指定会默认输出到/system/lib目录下。
根据前面HAL框架分析可知,HAL Stub库默认加载地址为:/vendor/lib/hw/或/system/lib/hw/,在这两个目录查找:硬件id名.default.so,所以我们这儿指定了HAL Stub的编译目标名为led.default,编译成动态库,输出目录为:$(TARGET_OUT_SHARED_LIBRARIES)/hw,TARGET_OUT_SHARED_LIBRARIES指/system/lib/目录。
5) 深入理解
我们从进程空间的概念来分析下我们上面写的代码。
我们前面的示例代码中,将LedDemo.java和LedService.java都放在了一个APK文件里,这也就意味着这个应用程序编译完之后,它会运行在一个Dalvik虚拟机实例中,即:一个进程里,在LedService.java中加载了libled_runtime.so库,通过JNI调用了本地代码,根据动态库的运行原理,我们知道,libled_runtime.so在第一次引用时会被加载到内存中并映射到引用库的进程空间中,我们可以简单理解为引用库的程序和被引用的库在一个进程中,而在libled_runtime.so库中,又通过dlopen打开了库文件led.default.so(该库并没有被库加载器加载,而是被当成一个文件打开的),同样我们可以理解为led.default.so和libled_runtime.so在同一个进程中。
由此可见,上面示例的Led HAL代码全部都在一个进程中实现,在该示例中的LedService功能比较多余,基本上不能算是一个服务。如果LedDemo运行在两个进程中,就意味着两个进程里的LedService不能复用,通常我们所谓的Service服务一般向客户端提供服务并且同时可以为多个客户端服务(如下图),所以我们的示例Led HAL代码不是完美的HAL模型,我们后面章节会再实现一个比较完美的HAL架构。
文章来源:华清远见嵌入式学院,原文地址:http://www.embedu.org/Column/Column757.htm
深入浅出 - Android系统移植与平台开发(十) - led HAL简单设计案例分析的更多相关文章
- 深入浅出 - Android系统移植与平台开发(一)
深入浅出 - Android系统移植与平台开发(一) 分类: Android移植2012-09-05 14:16 16173人阅读 评论(12) 收藏 举报 androidgitgooglejdkub ...
- 深入浅出 - Android系统移植与平台开发(十)- Android编译系统与定制Android平台系统(瘋耔修改篇二)
第四章.Android编译系统与定制Android平台系统 4.1Android编译系统 Android的源码由几十万个文件构成,这些文件之间有的相互依赖,有的又相互独立,它们按功能或类型又被放到不同 ...
- 深入浅出 - Android系统移植与平台开发(十二)- Android JNI机制
第五章.JNI机制 4.1 JNI概述 由前面基础知识可知,Android的应用层由Java语言编写,Framework框架层则是由Java代码与C/C++语言实现,之所以由两种不同的语言组合开发框架 ...
- 深入浅出 - Android系统移植与平台开发(三)- 编译并运行Android4.0模拟器
作者:唐老师,华清远见嵌入式学院讲师. 1. 编译Android模拟器 在Ubuntu下,我们可以在源码里编译出自己的模拟器及SDK等编译工具,当然这个和在windows里下载的看起来没有什么区别 ...
- 深入浅出 - Android系统移植与平台开发(五)- 编译Android源码(转)
2.3编译Android源码 Android源码体积非常庞大,由Dalvik虚拟机.Linux内核.编译系统.框架代码.Android定制C库.测试套件.系统应用程序等部分组成,在编译Android源 ...
- 深入浅出 - Android系统移植与平台开发(十一)- Android系统的定制(瘋耔修改篇一)
首先非常感谢原文作者为我们提供的知识库,因为有你们的贡献,我们的开发难度更显简单 原文 : http://blog.csdn.net/mr_raptor/article/details/30113 ...
- 深入浅出 - Android系统移植与平台开发(二) - 准备Android开发环境
作者:唐老师,华清远见嵌入式学院讲师. 编译Android源码 关于android系统的编译,Android的官方网站上也给出了详细的说明.http://source.android.com/sour ...
- 深入浅出 - Android系统移植与平台开发(六)- 为Android启动加速
作者:唐老师,华清远见嵌入式学院讲师. Android的启动速度一直以来是他的诟病,虽然现在Android设备的硬件速度越来越快,但是随着新 版本的出现,其启动速度一直都比较慢,当然,作为程序员,我们 ...
- 深入浅出 - Android系统移植与平台开发(七)- 初识HAL
作者:唐老师,华清远见嵌入式学院讲师. 1. HAL的module与stub HAL(Hardware AbstractLayer)硬件抽象层是Google开发的Android系统里上层应用对底层硬件 ...
随机推荐
- MS SQL 合并结果集并求和 分类: SQL Server 数据库 2015-02-13 10:59 92人阅读 评论(0) 收藏
业务情景:有这样一张表:其中Id列为表主键,Name为用户名,State为记录的状态值,Note为状态的说明,方便阅读. 需求描述:需要查询出这样的结果:某个人某种状态的记录数,如:张三,待审核记录数 ...
- PMP 第三章 单个项目的项目管理标准
1 项目管理五大过程组分别是什么? 启动过程组 规划过程组 执行过程组 监控过程组 收尾过程组 2 启动项目组是干什么?包含哪些过程?每个阶段都需要启动吗? 启动过程组:获得授权,定义一个新项目或现有 ...
- 在Salesforce中编写Unit Test
Unit Test 也是一个 Class 文件,所以在创建 Unit Test 的时候要选择 Test Class 类型来创建,请看如下截图(在Eclipse中): 编写 Unit Test 基本流程 ...
- Android自动化压力测试之Monkey Test 异常解读(五)
monkey结果分类 monkey结果详细解读 monkey运行log输出后,得读懂日志内容,定位错误 lgo日志顺序输出分别为 测试命令信息.随机事件流(11种事件).异常信息(anr.crash ...
- 配置tomcat下war包可以自压缩
<Host name="localhost" appBase="/home/hark/web" unpackWARs="true" a ...
- 【转】清理Kylin的中间存储数据(HDFS & HBase Tables)
http://blog.csdn.net/jiangshouzhuang/article/details/51290399 Kylin在创建cube过程中会在HDFS上生成中间数据.另外,当我们对cu ...
- CSS3_实现圆角效果box-shadow
1.outline的直角与圆角 来给个div: <div class="use-outline"></div> 来再给个样式: .use-outline{ ...
- AngularJS 学习之事件
1.ng-click指令:定义了AngularJS点击事件 <div ng-app="" ng-controller="myCtrl"> <b ...
- 车销 商场 批发零售无线POS开单 智能POS开单打印 开单APP应用-云POS扫描打印一体方案
PDA数据采集器,是一款移动手持开单设备,它通过WIFI和GPRS连接并访问电脑,从进销存软件中读取数据,实现移动开单,打破电脑开单模式. 它自带扫描器,可直接扫描条码来查找产品,且功能强大.操作简单 ...
- jQuery-表格以及表单
表单应用: 1.设置高度: $comment.height($comment.height() + 50); $comment.animate({height : "+=50"}, ...